Skip to content

Commit

Permalink
suppress ExecutionContext by default in TestServer
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 committed May 9, 2019
1 parent b03bca1 commit 003d224
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 12 deletions.
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 Down Expand Up @@ -117,7 +119,7 @@ protected override async Task<HttpResponseMessage> SendAsync(
responseBody = context.Response.Body;
});

var httpContext = await contextBuilder.SendAsync(cancellationToken);
var httpContext = await contextBuilder.SendAsync(cancellationToken, PreserveExecutionContext);

var response = new HttpResponseMessage();
response.StatusCode = (HttpStatusCode)httpContext.Response.StatusCode;
Expand Down
26 changes: 19 additions & 7 deletions src/Hosting/TestHost/src/HttpContextBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ internal class HttpContextBuilder : IHttpBodyControlFeature
{
private readonly IHttpApplication<Context> _application;
private readonly HttpContext _httpContext;

private TaskCompletionSource<HttpContext> _responseTcs = new TaskCompletionSource<HttpContext>(TaskCreationOptions.RunContinuationsAsynchronously);
private ResponseStream _responseStream;
private ResponseFeature _responseFeature = new ResponseFeature();
Expand Down Expand Up @@ -57,15 +57,17 @@ internal void Configure(Action<HttpContext> configureContext)
/// Start processing the request.
/// </summary>
/// <returns></returns>
internal Task<HttpContext> SendAsync(CancellationToken cancellationToken)
internal Task<HttpContext> SendAsync(CancellationToken cancellationToken, bool preserveExecutionContext)
{
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 ExecutionContext (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 ExecutionContext. If it happens out
_testContext = _application.CreateContext(_httpContext.Features);

try
{
await _application.ProcessRequestAsync(_testContext);
Expand All @@ -81,7 +83,17 @@ 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
11 changes: 8 additions & 3 deletions src/Hosting/TestHost/src/TestServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ public IWebHost Host
/// </remarks>
public bool AllowSynchronousIO { get; set; } = false;

/// <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.
/// </summary>
public bool PreserveExecutionContext { get; set; } = false;

private IHttpApplication<Context> Application
{
get => _application ?? throw new InvalidOperationException("The server has not been started or no web application was configured.");
Expand All @@ -93,7 +98,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 +109,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 Down Expand Up @@ -147,7 +152,7 @@ public async Task<HttpContext> SendAsync(Action<HttpContext> configureContext, C
});
builder.Configure(configureContext);
// TODO: Wrap the request body if any?
return await builder.SendAsync(cancellationToken).ConfigureAwait(false);
return await builder.SendAsync(cancellationToken, PreserveExecutionContext).ConfigureAwait(false);
}

public void Dispose()
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,6 +47,7 @@ public Action<HttpRequest> ConfigureRequest
}

internal bool AllowSynchronousIO { get; set; }
internal bool PreserveExecutionContext { get; set; }

public async Task<WebSocket> ConnectAsync(Uri uri, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -86,7 +87,7 @@ public async Task<WebSocket> ConnectAsync(Uri uri, CancellationToken cancellatio
ConfigureRequest?.Invoke(context.Request);
});

var httpContext = await contextBuilder.SendAsync(cancellationToken);
var httpContext = await contextBuilder.SendAsync(cancellationToken, PreserveExecutionContext);

if (httpContext.Response.StatusCode != StatusCodes.Status101SwitchingProtocols)
{
Expand Down
53 changes: 53 additions & 0 deletions src/Hosting/TestHost/test/TestServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,59 @@ public async Task ManuallySetHostWinsOverInferredHostFromRequestUri(string uri)
Assert.Equal("otherhost:5678", responseBody);
}

[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);
}

public class TestDiagnosticListener
{
public class OnBeginRequestEventData
Expand Down
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 003d224

Please sign in to comment.