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:
parent
0b590ff46f
commit
d3dc92ff6c
|
|
@ -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 Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]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.HttpMessageHandler CreateHandler() { throw null; }
|
||||
public Microsoft.AspNetCore.TestHost.RequestBuilder CreateRequest(string path) { throw null; }
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
|
||||
internal bool AllowSynchronousIO { get; set; }
|
||||
|
||||
internal bool PreserveExecutionContext { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This adapts HttpRequestMessages to ASP.NET Core requests, dispatches them through the pipeline, and returns the
|
||||
/// associated HttpResponseMessage.
|
||||
|
|
@ -61,7 +63,7 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
|
||||
var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO);
|
||||
var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO, PreserveExecutionContext);
|
||||
|
||||
Stream responseBody = null;
|
||||
var requestContent = request.Content ?? new StreamContent(Stream.Null);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
|
|
@ -14,8 +15,9 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
internal class HttpContextBuilder : IHttpBodyControlFeature
|
||||
{
|
||||
private readonly IHttpApplication<Context> _application;
|
||||
private readonly bool _preserveExecutionContext;
|
||||
private readonly HttpContext _httpContext;
|
||||
|
||||
|
||||
private TaskCompletionSource<HttpContext> _responseTcs = new TaskCompletionSource<HttpContext>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private ResponseStream _responseStream;
|
||||
private ResponseFeature _responseFeature = new ResponseFeature();
|
||||
|
|
@ -23,10 +25,11 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
private bool _pipelineFinished;
|
||||
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));
|
||||
AllowSynchronousIO = allowSynchronousIO;
|
||||
_preserveExecutionContext = preserveExecutionContext;
|
||||
_httpContext = new DefaultHttpContext();
|
||||
|
||||
var request = _httpContext.Request;
|
||||
|
|
@ -61,11 +64,14 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
{
|
||||
var registration = cancellationToken.Register(AbortRequest);
|
||||
|
||||
_testContext = _application.CreateContext(_httpContext.Features);
|
||||
|
||||
// Async offload, don't let the test code block the caller.
|
||||
_ = Task.Factory.StartNew(async () =>
|
||||
// Everything inside this function happens in the SERVER's execution context (unless PreserveExecutionContext is true)
|
||||
async Task RunRequestAsync()
|
||||
{
|
||||
// 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
|
||||
{
|
||||
await _application.ProcessRequestAsync(_testContext);
|
||||
|
|
@ -81,7 +87,20 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
{
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,12 +78,14 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
public IFeatureCollection Features { get; }
|
||||
|
||||
/// <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>
|
||||
/// <remarks>
|
||||
/// Defaults to false.
|
||||
/// </remarks>
|
||||
public bool AllowSynchronousIO { get; set; } = false;
|
||||
public bool AllowSynchronousIO { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
{
|
||||
|
|
@ -93,7 +95,7 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
public HttpMessageHandler CreateHandler()
|
||||
{
|
||||
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()
|
||||
|
|
@ -104,7 +106,7 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
public WebSocketClient CreateWebSocketClient()
|
||||
{
|
||||
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>
|
||||
|
|
@ -128,7 +130,7 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
throw new ArgumentNullException(nameof(configureContext));
|
||||
}
|
||||
|
||||
var builder = new HttpContextBuilder(Application, AllowSynchronousIO);
|
||||
var builder = new HttpContextBuilder(Application, AllowSynchronousIO, PreserveExecutionContext);
|
||||
builder.Configure(context =>
|
||||
{
|
||||
var request = context.Request;
|
||||
|
|
|
|||
|
|
@ -47,11 +47,12 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
}
|
||||
|
||||
internal bool AllowSynchronousIO { get; set; }
|
||||
internal bool PreserveExecutionContext { get; set; }
|
||||
|
||||
public async Task<WebSocket> ConnectAsync(Uri uri, CancellationToken cancellationToken)
|
||||
{
|
||||
WebSocketFeature webSocketFeature = null;
|
||||
var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO);
|
||||
var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO, PreserveExecutionContext);
|
||||
contextBuilder.Configure(context =>
|
||||
{
|
||||
var request = context.Request;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
|
|
@ -424,5 +425,58 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
// Assert
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,5 +15,10 @@
|
|||
<ItemGroup>
|
||||
<Reference Include="System.ServiceProcess.ServiceController" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="WebHostService.cs">
|
||||
<SubType>Component</SubType>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
Loading…
Reference in New Issue