diff --git a/src/Hosting/TestHost/ref/Microsoft.AspNetCore.TestHost.netcoreapp3.0.cs b/src/Hosting/TestHost/ref/Microsoft.AspNetCore.TestHost.netcoreapp3.0.cs index a51910e1ad..371fdc2135 100644 --- a/src/Hosting/TestHost/ref/Microsoft.AspNetCore.TestHost.netcoreapp3.0.cs +++ b/src/Hosting/TestHost/ref/Microsoft.AspNetCore.TestHost.netcoreapp3.0.cs @@ -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; } diff --git a/src/Hosting/TestHost/src/ClientHandler.cs b/src/Hosting/TestHost/src/ClientHandler.cs index edc28b471c..fb71cc81b4 100644 --- a/src/Hosting/TestHost/src/ClientHandler.cs +++ b/src/Hosting/TestHost/src/ClientHandler.cs @@ -45,6 +45,8 @@ namespace Microsoft.AspNetCore.TestHost internal bool AllowSynchronousIO { get; set; } + internal bool PreserveExecutionContext { get; set; } + /// /// 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); diff --git a/src/Hosting/TestHost/src/HttpContextBuilder.cs b/src/Hosting/TestHost/src/HttpContextBuilder.cs index 61356e5be5..9fd2beb547 100644 --- a/src/Hosting/TestHost/src/HttpContextBuilder.cs +++ b/src/Hosting/TestHost/src/HttpContextBuilder.cs @@ -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 _application; + private readonly bool _preserveExecutionContext; private readonly HttpContext _httpContext; - + private TaskCompletionSource _responseTcs = new TaskCompletionSource(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 application, bool allowSynchronousIO) + internal HttpContextBuilder(IHttpApplication 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; } diff --git a/src/Hosting/TestHost/src/TestServer.cs b/src/Hosting/TestHost/src/TestServer.cs index 783184a8de..07932f7457 100644 --- a/src/Hosting/TestHost/src/TestServer.cs +++ b/src/Hosting/TestHost/src/TestServer.cs @@ -78,12 +78,14 @@ namespace Microsoft.AspNetCore.TestHost public IFeatureCollection Features { get; } /// - /// Gets or sets a value that controls whether synchronous IO is allowed for the and + /// Gets or sets a value that controls whether synchronous IO is allowed for the and . The default value is . /// - /// - /// Defaults to false. - /// - public bool AllowSynchronousIO { get; set; } = false; + public bool AllowSynchronousIO { get; set; } + + /// + /// Gets or sets a value that controls if and values are preserved from the client to the server. The default value is . + /// + public bool PreserveExecutionContext { get; set; } private IHttpApplication 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 }; } /// @@ -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; diff --git a/src/Hosting/TestHost/src/WebSocketClient.cs b/src/Hosting/TestHost/src/WebSocketClient.cs index 40a904cc57..4d1a6740d3 100644 --- a/src/Hosting/TestHost/src/WebSocketClient.cs +++ b/src/Hosting/TestHost/src/WebSocketClient.cs @@ -47,11 +47,12 @@ namespace Microsoft.AspNetCore.TestHost } internal bool AllowSynchronousIO { get; set; } + internal bool PreserveExecutionContext { get; set; } public async Task 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; diff --git a/src/Hosting/TestHost/test/TestClientTests.cs b/src/Hosting/TestHost/test/TestClientTests.cs index 7b86c18978..20a45706dd 100644 --- a/src/Hosting/TestHost/test/TestClientTests.cs +++ b/src/Hosting/TestHost/test/TestClientTests.cs @@ -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(async () => await tcs.Task); } + + [Fact] + public async Task AsyncLocalValueOnClientIsNotPreserved() + { + var asyncLocal = new AsyncLocal(); + 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(); + 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); + } } } diff --git a/src/Hosting/WindowsServices/src/Microsoft.AspNetCore.Hosting.WindowsServices.csproj b/src/Hosting/WindowsServices/src/Microsoft.AspNetCore.Hosting.WindowsServices.csproj index 43aabd97a9..00037213e0 100644 --- a/src/Hosting/WindowsServices/src/Microsoft.AspNetCore.Hosting.WindowsServices.csproj +++ b/src/Hosting/WindowsServices/src/Microsoft.AspNetCore.Hosting.WindowsServices.csproj @@ -15,5 +15,10 @@ + + + Component + +