Skip to content

Commit

Permalink
suppress ExecutionContext by default in TestServer (#10094)
Browse files Browse the repository at this point in the history
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!
  • Loading branch information
analogrelay authored May 10, 2019
1 parent 0b590ff commit d3dc92f
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public TestServer(Microsoft.AspNetCore.Http.Features.IFeatureCollection featureC
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; }
Expand Down
4 changes: 3 additions & 1 deletion src/Hosting/TestHost/src/ClientHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ internal ClientHandler(PathString pathBase, IHttpApplication<Context> applicatio

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.
Expand All @@ -61,7 +63,7 @@ protected override async Task<HttpResponseMessage> SendAsync(
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);
Expand Down
33 changes: 26 additions & 7 deletions src/Hosting/TestHost/src/HttpContextBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,19 +15,21 @@ 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();
private RequestLifetimeFeature _requestLifetimeFeature = new RequestLifetimeFeature();
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;
Expand Down Expand Up @@ -61,11 +64,14 @@ internal Task<HttpContext> SendAsync(CancellationToken cancellationToken)
{
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);
Expand All @@ -81,7 +87,20 @@ internal Task<HttpContext> SendAsync(CancellationToken cancellationToken)
{
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;
}
Expand Down
18 changes: 10 additions & 8 deletions src/Hosting/TestHost/src/TestServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,14 @@ public IWebHost Host
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
{
Expand All @@ -93,7 +95,7 @@ private IHttpApplication<Context> Application
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()
Expand All @@ -104,7 +106,7 @@ public HttpClient CreateClient()
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>
Expand All @@ -128,7 +130,7 @@ public async Task<HttpContext> SendAsync(Action<HttpContext> configureContext, C
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;
Expand Down
3 changes: 2 additions & 1 deletion src/Hosting/TestHost/src/WebSocketClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ public Action<HttpRequest> ConfigureRequest
}

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;
Expand Down
54 changes: 54 additions & 0 deletions src/Hosting/TestHost/test/TestClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -424,5 +425,58 @@ public async Task ClientCancellationAbortsRequest()
// 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,10 @@
<ItemGroup>
<Reference Include="System.ServiceProcess.ServiceController" />
</ItemGroup>
<ItemGroup>
<Compile Update="WebHostService.cs">
<SubType>Component</SubType>
</Compile>
</ItemGroup>

</Project>

0 comments on commit d3dc92f

Please sign in to comment.