From 1f892d798d3163b4bd9d3c4e900f6bb5c2310f9c Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Tue, 15 Jan 2019 11:48:17 -0800 Subject: [PATCH] Add AllowSynchronousIO to TestServer and IIS, fix tests (#6404) --- .../TestHost/src/AsyncStreamWrapper.cs | 128 ++++++++++++++++++ src/Hosting/TestHost/src/ClientHandler.cs | 6 +- .../TestHost/src/HttpContextBuilder.cs | 14 +- src/Hosting/TestHost/src/ResponseStream.cs | 9 +- src/Hosting/TestHost/src/TestServer.cs | 15 +- src/Hosting/TestHost/src/WebSocketClient.cs | 6 +- .../TestHost/test/ClientHandlerTests.cs | 5 +- .../TestHost/test/HttpContextBuilderTests.cs | 1 + src/Hosting/TestHost/test/TestClientTests.cs | 13 +- .../test/InMemory.Test/FunctionalTest.cs | 8 +- .../SampleDestination/SampleMiddleware.cs | 4 +- .../CORS/samples/SampleDestination/Startup.cs | 4 +- .../ResponseCaching/test/TestUtils.cs | 7 +- .../test/ResponseCompressionMiddlewareTest.cs | 12 ++ .../UrlActions/CustomResponseAction.cs | 9 +- .../Authentication/test/CookieTests.cs | 10 +- .../Authentication/test/DynamicSchemeTests.cs | 2 +- .../Authentication/test/GoogleTests.cs | 10 +- .../test/MicrosoftAccountTests.cs | 2 +- .../Authentication/test/PolicyTests.cs | 2 +- .../Authentication/test/TestExtensions.cs | 8 +- .../CookiePolicy/test/TestExtensions.cs | 19 --- .../Listener/ResponseBodyTests.cs | 1 + .../FunctionalTests/ResponseCachingTests.cs | 2 +- .../IIS/IIS/src/Core/IISHttpContext.cs | 2 + src/Servers/IIS/IIS/src/IISServerOptions.cs | 10 ++ .../Inprocess/CompressionTests.cs | 4 +- .../testassets/InProcessWebSite/Startup.cs | 12 +- .../Http2/Http2StreamTests.cs | 4 +- src/Shared/RazorViews/BaseView.cs | 8 +- 30 files changed, 253 insertions(+), 84 deletions(-) create mode 100644 src/Hosting/TestHost/src/AsyncStreamWrapper.cs diff --git a/src/Hosting/TestHost/src/AsyncStreamWrapper.cs b/src/Hosting/TestHost/src/AsyncStreamWrapper.cs new file mode 100644 index 0000000000..32f1548d35 --- /dev/null +++ b/src/Hosting/TestHost/src/AsyncStreamWrapper.cs @@ -0,0 +1,128 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.TestHost +{ + internal class AsyncStreamWrapper : Stream + { + private Stream _inner; + private Func _allowSynchronousIO; + + internal AsyncStreamWrapper(Stream inner, Func allowSynchronousIO) + { + _inner = inner; + _allowSynchronousIO = allowSynchronousIO; + } + + public override bool CanRead => _inner.CanRead; + + public override bool CanSeek => _inner.CanSeek; + + public override bool CanWrite => _inner.CanWrite; + + public override long Length => _inner.Length; + + public override long Position { get => _inner.Position; set => _inner.Position = value; } + + public override void Flush() + { + // Not blocking Flush because things like StreamWriter.Dispose() always call it. + _inner.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _inner.FlushAsync(cancellationToken); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (!_allowSynchronousIO()) + { + throw new InvalidOperationException("Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true."); + } + + return _inner.Read(buffer, offset, count); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.ReadAsync(buffer, offset, count, cancellationToken); + } + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + return _inner.ReadAsync(buffer, cancellationToken); + } + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return _inner.BeginRead(buffer, offset, count, callback, state); + } + + public override int EndRead(IAsyncResult asyncResult) + { + return _inner.EndRead(asyncResult); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _inner.Seek(offset, origin); + } + + public override void SetLength(long value) + { + _inner.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (!_allowSynchronousIO()) + { + throw new InvalidOperationException("Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true."); + } + + _inner.Write(buffer, offset, count); + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return _inner.BeginWrite(buffer, offset, count, callback, state); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + _inner.EndWrite(asyncResult); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.WriteAsync(buffer, offset, count, cancellationToken); + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + return _inner.WriteAsync(buffer, cancellationToken); + } + + public override void Close() + { + _inner.Close(); + } + + protected override void Dispose(bool disposing) + { + _inner.Dispose(); + } + + public override ValueTask DisposeAsync() + { + return _inner.DisposeAsync(); + } + } +} diff --git a/src/Hosting/TestHost/src/ClientHandler.cs b/src/Hosting/TestHost/src/ClientHandler.cs index 5471e23d19..c947623220 100644 --- a/src/Hosting/TestHost/src/ClientHandler.cs +++ b/src/Hosting/TestHost/src/ClientHandler.cs @@ -43,6 +43,8 @@ namespace Microsoft.AspNetCore.TestHost _pathBase = pathBase; } + internal bool AllowSynchronousIO { get; set; } + /// /// This adapts HttpRequestMessages to ASP.NET Core requests, dispatches them through the pipeline, and returns the /// associated HttpResponseMessage. @@ -59,7 +61,7 @@ namespace Microsoft.AspNetCore.TestHost throw new ArgumentNullException(nameof(request)); } - var contextBuilder = new HttpContextBuilder(_application); + var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO); Stream responseBody = null; var requestContent = request.Content ?? new StreamContent(Stream.Null); @@ -110,7 +112,7 @@ namespace Microsoft.AspNetCore.TestHost // This body may have been consumed before, rewind it. body.Seek(0, SeekOrigin.Begin); } - req.Body = body; + req.Body = new AsyncStreamWrapper(body, () => contextBuilder.AllowSynchronousIO); responseBody = context.Response.Body; }); diff --git a/src/Hosting/TestHost/src/HttpContextBuilder.cs b/src/Hosting/TestHost/src/HttpContextBuilder.cs index c576628b65..69acf27591 100644 --- a/src/Hosting/TestHost/src/HttpContextBuilder.cs +++ b/src/Hosting/TestHost/src/HttpContextBuilder.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -11,7 +11,7 @@ using static Microsoft.AspNetCore.Hosting.Internal.HostingApplication; namespace Microsoft.AspNetCore.TestHost { - internal class HttpContextBuilder + internal class HttpContextBuilder : IHttpBodyControlFeature { private readonly IHttpApplication _application; private readonly HttpContext _httpContext; @@ -23,24 +23,28 @@ namespace Microsoft.AspNetCore.TestHost private bool _pipelineFinished; private Context _testContext; - internal HttpContextBuilder(IHttpApplication application) + internal HttpContextBuilder(IHttpApplication application, bool allowSynchronousIO) { _application = application ?? throw new ArgumentNullException(nameof(application)); + AllowSynchronousIO = allowSynchronousIO; _httpContext = new DefaultHttpContext(); var request = _httpContext.Request; request.Protocol = "HTTP/1.1"; request.Method = HttpMethods.Get; + _httpContext.Features.Set(this); _httpContext.Features.Set(_responseFeature); var requestLifetimeFeature = new HttpRequestLifetimeFeature(); requestLifetimeFeature.RequestAborted = _requestAbortedSource.Token; _httpContext.Features.Set(requestLifetimeFeature); - _responseStream = new ResponseStream(ReturnResponseMessageAsync, AbortRequest); + _responseStream = new ResponseStream(ReturnResponseMessageAsync, AbortRequest, () => AllowSynchronousIO); _responseFeature.Body = _responseStream; } + public bool AllowSynchronousIO { get; set; } + internal void Configure(Action configureContext) { if (configureContext == null) @@ -136,4 +140,4 @@ namespace Microsoft.AspNetCore.TestHost _responseTcs.TrySetException(exception); } } -} \ No newline at end of file +} diff --git a/src/Hosting/TestHost/src/ResponseStream.cs b/src/Hosting/TestHost/src/ResponseStream.cs index b2ababa182..7563beb4c3 100644 --- a/src/Hosting/TestHost/src/ResponseStream.cs +++ b/src/Hosting/TestHost/src/ResponseStream.cs @@ -24,13 +24,15 @@ namespace Microsoft.AspNetCore.TestHost private Func _onFirstWriteAsync; private bool _firstWrite; private Action _abortRequest; + private Func _allowSynchronousIO; private Pipe _pipe = new Pipe(); - internal ResponseStream(Func onFirstWriteAsync, Action abortRequest) + internal ResponseStream(Func onFirstWriteAsync, Action abortRequest, Func allowSynchronousIO) { _onFirstWriteAsync = onFirstWriteAsync ?? throw new ArgumentNullException(nameof(onFirstWriteAsync)); _abortRequest = abortRequest ?? throw new ArgumentNullException(nameof(abortRequest)); + _allowSynchronousIO = allowSynchronousIO ?? throw new ArgumentNullException(nameof(allowSynchronousIO)); _firstWrite = true; _writeLock = new SemaphoreSlim(1, 1); } @@ -144,6 +146,11 @@ namespace Microsoft.AspNetCore.TestHost // Write with count 0 will still trigger OnFirstWrite public override void Write(byte[] buffer, int offset, int count) { + if (!_allowSynchronousIO()) + { + throw new InvalidOperationException("Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true."); + } + // The Pipe Write method requires calling FlushAsync to notify the reader. Call WriteAsync instead. WriteAsync(buffer, offset, count).GetAwaiter().GetResult(); } diff --git a/src/Hosting/TestHost/src/TestServer.cs b/src/Hosting/TestHost/src/TestServer.cs index 6c45c0b21b..c5668185ac 100644 --- a/src/Hosting/TestHost/src/TestServer.cs +++ b/src/Hosting/TestHost/src/TestServer.cs @@ -77,6 +77,14 @@ namespace Microsoft.AspNetCore.TestHost public IFeatureCollection Features { get; } + /// + /// Gets or sets a value that controls whether synchronous IO is allowed for the and + /// + /// + /// Defaults to true. + /// + public bool AllowSynchronousIO { get; set; } = true; + private IHttpApplication Application { get => _application ?? throw new InvalidOperationException("The server has not been started or no web application was configured."); @@ -85,7 +93,7 @@ namespace Microsoft.AspNetCore.TestHost public HttpMessageHandler CreateHandler() { var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress); - return new ClientHandler(pathBase, Application); + return new ClientHandler(pathBase, Application) { AllowSynchronousIO = AllowSynchronousIO }; } public HttpClient CreateClient() @@ -96,7 +104,7 @@ namespace Microsoft.AspNetCore.TestHost public WebSocketClient CreateWebSocketClient() { var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress); - return new WebSocketClient(pathBase, Application); + return new WebSocketClient(pathBase, Application) { AllowSynchronousIO = AllowSynchronousIO }; } /// @@ -120,7 +128,7 @@ namespace Microsoft.AspNetCore.TestHost throw new ArgumentNullException(nameof(configureContext)); } - var builder = new HttpContextBuilder(Application); + var builder = new HttpContextBuilder(Application, AllowSynchronousIO); builder.Configure(context => { var request = context.Request; @@ -138,6 +146,7 @@ namespace Microsoft.AspNetCore.TestHost request.PathBase = pathBase; }); builder.Configure(configureContext); + // TODO: Wrap the request body if any? return await builder.SendAsync(cancellationToken).ConfigureAwait(false); } diff --git a/src/Hosting/TestHost/src/WebSocketClient.cs b/src/Hosting/TestHost/src/WebSocketClient.cs index e3deb670a5..cec96ab4ce 100644 --- a/src/Hosting/TestHost/src/WebSocketClient.cs +++ b/src/Hosting/TestHost/src/WebSocketClient.cs @@ -46,10 +46,12 @@ namespace Microsoft.AspNetCore.TestHost set; } + internal bool AllowSynchronousIO { get; set; } + public async Task ConnectAsync(Uri uri, CancellationToken cancellationToken) { WebSocketFeature webSocketFeature = null; - var contextBuilder = new HttpContextBuilder(_application); + var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO); contextBuilder.Configure(context => { var request = context.Request; @@ -131,4 +133,4 @@ namespace Microsoft.AspNetCore.TestHost } } } -} \ No newline at end of file +} diff --git a/src/Hosting/TestHost/test/ClientHandlerTests.cs b/src/Hosting/TestHost/test/ClientHandlerTests.cs index f287ebd053..012867dd28 100644 --- a/src/Hosting/TestHost/test/ClientHandlerTests.cs +++ b/src/Hosting/TestHost/test/ClientHandlerTests.cs @@ -92,13 +92,12 @@ namespace Microsoft.AspNetCore.TestHost public async Task ResubmitRequestWorks() { int requestCount = 1; - var handler = new ClientHandler(PathString.Empty, new DummyApplication(context => + var handler = new ClientHandler(PathString.Empty, new DummyApplication(async context => { - int read = context.Request.Body.Read(new byte[100], 0, 100); + int read = await context.Request.Body.ReadAsync(new byte[100], 0, 100); Assert.Equal(11, read); context.Response.Headers["TestHeader"] = "TestValue:" + requestCount++; - return Task.FromResult(0); })); HttpMessageInvoker invoker = new HttpMessageInvoker(handler); diff --git a/src/Hosting/TestHost/test/HttpContextBuilderTests.cs b/src/Hosting/TestHost/test/HttpContextBuilderTests.cs index f04a2f16f9..88126de9e8 100644 --- a/src/Hosting/TestHost/test/HttpContextBuilderTests.cs +++ b/src/Hosting/TestHost/test/HttpContextBuilderTests.cs @@ -109,6 +109,7 @@ namespace Microsoft.AspNetCore.TestHost { c.Response.Headers["TestHeader"] = "TestValue"; var bytes = Encoding.UTF8.GetBytes("BodyStarted" + Environment.NewLine); + c.Features.Get().AllowSynchronousIO = true; c.Response.Body.Write(bytes, 0, bytes.Length); await block.Task; bytes = Encoding.UTF8.GetBytes("BodyFinished"); diff --git a/src/Hosting/TestHost/test/TestClientTests.cs b/src/Hosting/TestHost/test/TestClientTests.cs index 3101c2965f..7b86c18978 100644 --- a/src/Hosting/TestHost/test/TestClientTests.cs +++ b/src/Hosting/TestHost/test/TestClientTests.cs @@ -87,8 +87,8 @@ namespace Microsoft.AspNetCore.TestHost public async Task PutAsyncWorks() { // Arrange - RequestDelegate appDelegate = ctx => - ctx.Response.WriteAsync(new StreamReader(ctx.Request.Body).ReadToEnd() + " PUT Response"); + RequestDelegate appDelegate = async ctx => + await ctx.Response.WriteAsync(await new StreamReader(ctx.Request.Body).ReadToEndAsync() + " PUT Response"); var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); var server = new TestServer(builder); var client = server.CreateClient(); @@ -106,7 +106,7 @@ namespace Microsoft.AspNetCore.TestHost { // Arrange RequestDelegate appDelegate = async ctx => - await ctx.Response.WriteAsync(new StreamReader(ctx.Request.Body).ReadToEnd() + " POST Response"); + await ctx.Response.WriteAsync(await new StreamReader(ctx.Request.Body).ReadToEndAsync() + " POST Response"); var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); var server = new TestServer(builder); var client = server.CreateClient(); @@ -132,16 +132,15 @@ namespace Microsoft.AspNetCore.TestHost } var builder = new WebHostBuilder(); - RequestDelegate app = (ctx) => + RequestDelegate app = async ctx => { var disposable = new TestDisposable(); ctx.Response.RegisterForDispose(disposable); - ctx.Response.Body.Write(data, 0, 1024); + await ctx.Response.Body.WriteAsync(data, 0, 1024); Assert.False(disposable.IsDisposed); - ctx.Response.Body.Write(data, 1024, 1024); - return Task.FromResult(0); + await ctx.Response.Body.WriteAsync(data, 1024, 1024); }; builder.Configure(appBuilder => appBuilder.Run(app)); diff --git a/src/Identity/test/InMemory.Test/FunctionalTest.cs b/src/Identity/test/InMemory.Test/FunctionalTest.cs index 7d3ab77494..b193bc16cd 100644 --- a/src/Identity/test/InMemory.Test/FunctionalTest.cs +++ b/src/Identity/test/InMemory.Test/FunctionalTest.cs @@ -352,12 +352,12 @@ namespace Microsoft.AspNetCore.Identity.InMemory } else if (req.Path == new PathString("/me")) { - Describe(res, AuthenticateResult.Success(new AuthenticationTicket(context.User, null, "Application"))); + await DescribeAsync(res, AuthenticateResult.Success(new AuthenticationTicket(context.User, null, "Application"))); } else if (req.Path.StartsWithSegments(new PathString("/me"), out remainder)) { var auth = await context.AuthenticateAsync(remainder.Value.Substring(1)); - Describe(res, auth); + await DescribeAsync(res, auth); } else if (req.Path == new PathString("/testpath") && testpath != null) { @@ -393,7 +393,7 @@ namespace Microsoft.AspNetCore.Identity.InMemory return server; } - private static void Describe(HttpResponse res, AuthenticateResult result) + private static async Task DescribeAsync(HttpResponse res, AuthenticateResult result) { res.StatusCode = 200; res.ContentType = "text/xml"; @@ -412,7 +412,7 @@ namespace Microsoft.AspNetCore.Identity.InMemory { xml.WriteTo(writer); } - res.Body.Write(memory.ToArray(), 0, memory.ToArray().Length); + await res.Body.WriteAsync(memory.ToArray(), 0, memory.ToArray().Length); } } diff --git a/src/Middleware/CORS/samples/SampleDestination/SampleMiddleware.cs b/src/Middleware/CORS/samples/SampleDestination/SampleMiddleware.cs index ac53eb5908..4400bd46d3 100644 --- a/src/Middleware/CORS/samples/SampleDestination/SampleMiddleware.cs +++ b/src/Middleware/CORS/samples/SampleDestination/SampleMiddleware.cs @@ -25,9 +25,7 @@ namespace SampleDestination context.Response.ContentType = "text/plain; charset=utf-8"; context.Response.ContentLength = content.Length; - context.Response.Body.Write(content, 0, content.Length); - - return Task.CompletedTask; + return context.Response.Body.WriteAsync(content, 0, content.Length); } } } diff --git a/src/Middleware/CORS/samples/SampleDestination/Startup.cs b/src/Middleware/CORS/samples/SampleDestination/Startup.cs index cf306c6eeb..b17c504ad8 100644 --- a/src/Middleware/CORS/samples/SampleDestination/Startup.cs +++ b/src/Middleware/CORS/samples/SampleDestination/Startup.cs @@ -86,9 +86,7 @@ namespace SampleDestination context.Response.ContentType = "text/plain; charset=utf-8"; context.Response.ContentLength = content.Length; - context.Response.Body.Write(content, 0, content.Length); - - return Task.CompletedTask; + return context.Response.Body.WriteAsync(content, 0, content.Length); } } } diff --git a/src/Middleware/ResponseCaching/test/TestUtils.cs b/src/Middleware/ResponseCaching/test/TestUtils.cs index 09f21a8878..319c1d590f 100644 --- a/src/Middleware/ResponseCaching/test/TestUtils.cs +++ b/src/Middleware/ResponseCaching/test/TestUtils.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -81,6 +81,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var uniqueId = Guid.NewGuid().ToString(); if (TestRequestDelegate(context, uniqueId)) { + var feature = context.Features.Get(); + if (feature != null) + { + feature.AllowSynchronousIO = true; + } context.Response.Write(uniqueId); } return Task.CompletedTask; diff --git a/src/Middleware/ResponseCompression/test/ResponseCompressionMiddlewareTest.cs b/src/Middleware/ResponseCompression/test/ResponseCompressionMiddlewareTest.cs index 43cf908475..4f5c6bf073 100644 --- a/src/Middleware/ResponseCompression/test/ResponseCompressionMiddlewareTest.cs +++ b/src/Middleware/ResponseCompression/test/ResponseCompressionMiddlewareTest.cs @@ -548,6 +548,12 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests app.UseResponseCompression(); app.Run(context => { + var feature = context.Features.Get(); + if (feature != null) + { + feature.AllowSynchronousIO = true; + } + context.Response.Headers[HeaderNames.ContentMD5] = "MD5"; context.Response.ContentType = TextPlain; context.Response.Body.Write(new byte[10], 0, 10); @@ -652,6 +658,12 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests context.Response.ContentType = TextPlain; context.Features.Get()?.DisableResponseBuffering(); + var feature = context.Features.Get(); + if (feature != null) + { + feature.AllowSynchronousIO = true; + } + foreach (var signal in responseReceived) { context.Response.Body.Write(new byte[1], 0, 1); diff --git a/src/Middleware/Rewrite/src/Internal/UrlActions/CustomResponseAction.cs b/src/Middleware/Rewrite/src/Internal/UrlActions/CustomResponseAction.cs index b7cc36eeda..4fc7d1615c 100644 --- a/src/Middleware/Rewrite/src/Internal/UrlActions/CustomResponseAction.cs +++ b/src/Middleware/Rewrite/src/Internal/UrlActions/CustomResponseAction.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Text; @@ -31,6 +31,11 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.UrlActions if (!string.IsNullOrEmpty(StatusDescription)) { + var feature = context.HttpContext.Features.Get(); + if (feature != null) + { + feature.AllowSynchronousIO = true; + } var content = Encoding.UTF8.GetBytes(StatusDescription); response.ContentLength = content.Length; response.ContentType = "text/plain; charset=utf-8"; @@ -42,4 +47,4 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.UrlActions context.Logger?.CustomResponse(context.HttpContext.Request.GetEncodedUrl()); } } -} \ No newline at end of file +} diff --git a/src/Security/Authentication/test/CookieTests.cs b/src/Security/Authentication/test/CookieTests.cs index 107fc5db1e..504a264b41 100644 --- a/src/Security/Authentication/test/CookieTests.cs +++ b/src/Security/Authentication/test/CookieTests.cs @@ -1302,7 +1302,7 @@ namespace Microsoft.AspNetCore.Authentication.Cookies app.Use(async (context, next) => { var result = await context.AuthenticateAsync("Cookies"); - Describe(context.Response, result); + await DescribeAsync(context.Response, result); }); }) .ConfigureServices(services => services.AddAuthentication().AddCookie("Cookies", o => @@ -1478,12 +1478,12 @@ namespace Microsoft.AspNetCore.Authentication.Cookies } else if (req.Path == new PathString("/me")) { - Describe(res, AuthenticateResult.Success(new AuthenticationTicket(context.User, new AuthenticationProperties(), CookieAuthenticationDefaults.AuthenticationScheme))); + await DescribeAsync(res, AuthenticateResult.Success(new AuthenticationTicket(context.User, new AuthenticationProperties(), CookieAuthenticationDefaults.AuthenticationScheme))); } else if (req.Path.StartsWithSegments(new PathString("/me"), out remainder)) { var ticket = await context.AuthenticateAsync(remainder.Value.Substring(1)); - Describe(res, ticket); + await DescribeAsync(res, ticket); } else if (req.Path == new PathString("/testpath") && testpath != null) { @@ -1510,7 +1510,7 @@ namespace Microsoft.AspNetCore.Authentication.Cookies return server; } - private static void Describe(HttpResponse res, AuthenticateResult result) + private static Task DescribeAsync(HttpResponse res, AuthenticateResult result) { res.StatusCode = 200; res.ContentType = "text/xml"; @@ -1524,7 +1524,7 @@ namespace Microsoft.AspNetCore.Authentication.Cookies xml.Add(result.Ticket.Properties.Items.Select(extra => new XElement("extra", new XAttribute("type", extra.Key), new XAttribute("value", extra.Value)))); } var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString()); - res.Body.Write(xmlBytes, 0, xmlBytes.Length); + return res.Body.WriteAsync(xmlBytes, 0, xmlBytes.Length); } private static async Task SendAsync(TestServer server, string uri, string cookieHeader = null) diff --git a/src/Security/Authentication/test/DynamicSchemeTests.cs b/src/Security/Authentication/test/DynamicSchemeTests.cs index d658609b04..6df65f66ad 100644 --- a/src/Security/Authentication/test/DynamicSchemeTests.cs +++ b/src/Security/Authentication/test/DynamicSchemeTests.cs @@ -145,7 +145,7 @@ namespace Microsoft.AspNetCore.Authentication { var name = (remainder.Value.Length > 0) ? remainder.Value.Substring(1) : null; var result = await context.AuthenticateAsync(name); - res.Describe(result?.Ticket?.Principal); + await res.DescribeAsync(result?.Ticket?.Principal); } else if (req.Path.StartsWithSegments(new PathString("/remove"), out remainder)) { diff --git a/src/Security/Authentication/test/GoogleTests.cs b/src/Security/Authentication/test/GoogleTests.cs index ce158eaf20..24b7ef0fab 100644 --- a/src/Security/Authentication/test/GoogleTests.cs +++ b/src/Security/Authentication/test/GoogleTests.cs @@ -1177,26 +1177,26 @@ namespace Microsoft.AspNetCore.Authentication.Google { var result = await context.AuthenticateAsync(TestExtensions.CookieAuthenticationScheme); var tokens = result.Properties.GetTokens(); - res.Describe(tokens); + await res.DescribeAsync(tokens); } else if (req.Path == new PathString("/me")) { - res.Describe(context.User); + await res.DescribeAsync(context.User); } else if (req.Path == new PathString("/authenticate")) { var result = await context.AuthenticateAsync(TestExtensions.CookieAuthenticationScheme); - res.Describe(result.Principal); + await res.DescribeAsync(result.Principal); } else if (req.Path == new PathString("/authenticateGoogle")) { var result = await context.AuthenticateAsync("Google"); - res.Describe(result?.Principal); + await res.DescribeAsync(result?.Principal); } else if (req.Path == new PathString("/authenticateFacebook")) { var result = await context.AuthenticateAsync("Facebook"); - res.Describe(result?.Principal); + await res.DescribeAsync(result?.Principal); } else if (req.Path == new PathString("/unauthorized")) { diff --git a/src/Security/Authentication/test/MicrosoftAccountTests.cs b/src/Security/Authentication/test/MicrosoftAccountTests.cs index 26a5484c83..4005b50efe 100644 --- a/src/Security/Authentication/test/MicrosoftAccountTests.cs +++ b/src/Security/Authentication/test/MicrosoftAccountTests.cs @@ -270,7 +270,7 @@ namespace Microsoft.AspNetCore.Authentication.Tests.MicrosoftAccount } else if (req.Path == new PathString("/me")) { - res.Describe(context.User); + await res.DescribeAsync(context.User); } else if (req.Path == new PathString("/signIn")) { diff --git a/src/Security/Authentication/test/PolicyTests.cs b/src/Security/Authentication/test/PolicyTests.cs index 368026beb8..60e6c41be6 100644 --- a/src/Security/Authentication/test/PolicyTests.cs +++ b/src/Security/Authentication/test/PolicyTests.cs @@ -469,7 +469,7 @@ namespace Microsoft.AspNetCore.Authentication { var name = (remainder.Value.Length > 0) ? remainder.Value.Substring(1) : null; var result = await context.AuthenticateAsync(name); - res.Describe(result?.Ticket?.Principal); + await res.DescribeAsync(result?.Ticket?.Principal); } else { diff --git a/src/Security/Authentication/test/TestExtensions.cs b/src/Security/Authentication/test/TestExtensions.cs index 87d6d95a2c..98b45a0159 100644 --- a/src/Security/Authentication/test/TestExtensions.cs +++ b/src/Security/Authentication/test/TestExtensions.cs @@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Authentication return transaction; } - public static void Describe(this HttpResponse res, ClaimsPrincipal principal) + public static Task DescribeAsync(this HttpResponse res, ClaimsPrincipal principal) { res.StatusCode = 200; res.ContentType = "text/xml"; @@ -62,10 +62,10 @@ namespace Microsoft.AspNetCore.Authentication } } var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString()); - res.Body.Write(xmlBytes, 0, xmlBytes.Length); + return res.Body.WriteAsync(xmlBytes, 0, xmlBytes.Length); } - public static void Describe(this HttpResponse res, IEnumerable tokens) + public static Task DescribeAsync(this HttpResponse res, IEnumerable tokens) { res.StatusCode = 200; res.ContentType = "text/xml"; @@ -79,7 +79,7 @@ namespace Microsoft.AspNetCore.Authentication } } var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString()); - res.Body.Write(xmlBytes, 0, xmlBytes.Length); + return res.Body.WriteAsync(xmlBytes, 0, xmlBytes.Length); } } diff --git a/src/Security/CookiePolicy/test/TestExtensions.cs b/src/Security/CookiePolicy/test/TestExtensions.cs index 9456094d41..4bbce1c302 100644 --- a/src/Security/CookiePolicy/test/TestExtensions.cs +++ b/src/Security/CookiePolicy/test/TestExtensions.cs @@ -45,24 +45,5 @@ namespace Microsoft.AspNetCore.CookiePolicy } return transaction; } - - public static void Describe(this HttpResponse res, ClaimsPrincipal principal) - { - res.StatusCode = 200; - res.ContentType = "text/xml"; - var xml = new XElement("xml"); - if (principal != null) - { - foreach (var identity in principal.Identities) - { - xml.Add(identity.Claims.Select(claim => - new XElement("claim", new XAttribute("type", claim.Type), - new XAttribute("value", claim.Value), - new XAttribute("issuer", claim.Issuer)))); - } - } - var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString()); - res.Body.Write(xmlBytes, 0, xmlBytes.Length); - } } } diff --git a/src/Servers/HttpSys/test/FunctionalTests/Listener/ResponseBodyTests.cs b/src/Servers/HttpSys/test/FunctionalTests/Listener/ResponseBodyTests.cs index ed3bfde554..af044d8297 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Listener/ResponseBodyTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/Listener/ResponseBodyTests.cs @@ -353,6 +353,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys.Listener context = await server.AcceptAsync(Utilities.DefaultTimeout).Before(responseTask); // First write sends headers + context.AllowSynchronousIO = true; context.Response.Body.Write(new byte[10], 0, 10); var response = await responseTask; diff --git a/src/Servers/HttpSys/test/FunctionalTests/ResponseCachingTests.cs b/src/Servers/HttpSys/test/FunctionalTests/ResponseCachingTests.cs index 7333a1ce71..73cb15f113 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/ResponseCachingTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/ResponseCachingTests.cs @@ -307,7 +307,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString(); httpContext.Response.Headers["Cache-Control"] = "public, max-age=10"; httpContext.Response.ContentLength = 10; - httpContext.Response.Body.Flush(); + httpContext.Response.Body.FlushAsync(); return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10); })) { diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs index fbb421d07a..1e20316d27 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs @@ -82,6 +82,8 @@ namespace Microsoft.AspNetCore.Server.IIS.Core _options = options; _server = server; _logger = logger; + + ((IHttpBodyControlFeature)this).AllowSynchronousIO = _options.AllowSynchronousIO; } public Version HttpVersion { get; set; } diff --git a/src/Servers/IIS/IIS/src/IISServerOptions.cs b/src/Servers/IIS/IIS/src/IISServerOptions.cs index f439080195..47d7619f8a 100644 --- a/src/Servers/IIS/IIS/src/IISServerOptions.cs +++ b/src/Servers/IIS/IIS/src/IISServerOptions.cs @@ -1,10 +1,20 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.AspNetCore.Http; + namespace Microsoft.AspNetCore.Builder { public class IISServerOptions { + /// + /// Gets or sets a value that controls whether synchronous IO is allowed for the and + /// + /// + /// Defaults to true. + /// + public bool AllowSynchronousIO { get; set; } = true; + /// /// If true the server should set HttpContext.User. If false the server will only provide an /// identity when explicitly requested by the AuthenticationScheme. diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/CompressionTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/CompressionTests.cs index ce1c84e609..09b1ecc077 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/CompressionTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/CompressionTests.cs @@ -81,10 +81,10 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip")); client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("identity", 0)); client.DefaultRequestHeaders.Add("Response-Content-Type", "text/event-stream"); - var messages = "Message1\r\nMessage2\r\n"; + var messages = "Message1\r\nMessage2\r\n\r\n"; // Send messages with terminator - var response = await client.PostAsync("ReadAndWriteEchoLines", new StringContent(messages + "\r\n")); + var response = await client.PostAsync("ReadAndWriteEchoLines", new StringContent(messages)); Assert.Equal(messages, await response.Content.ReadAsStringAsync()); Assert.True(response.Content.Headers.TryGetValues("Content-Type", out var contentTypes)); Assert.Single(contentTypes, "text/event-stream"); diff --git a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs index fb505a14ec..31f69a3b63 100644 --- a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs +++ b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs @@ -331,10 +331,10 @@ namespace TestSite await ctx.Response.Body.FlushAsync(); var reader = new StreamReader(ctx.Request.Body); - while (!reader.EndOfStream) + while (true) { var line = await reader.ReadLineAsync(); - if (line == "") + if (line == null) { return; } @@ -357,10 +357,10 @@ namespace TestSite await ctx.Response.Body.FlushAsync(); var reader = new StreamReader(ctx.Request.Body); - while (!reader.EndOfStream) + while (true) { var line = await reader.ReadLineAsync(); - if (line == "") + if (line == null) { return; } @@ -438,8 +438,8 @@ namespace TestSite private async Task TestReadOffsetWorks(HttpContext ctx) { var buffer = new byte[11]; - ctx.Request.Body.Read(buffer, 0, 6); - ctx.Request.Body.Read(buffer, 6, 5); + await ctx.Request.Body.ReadAsync(buffer, 0, 6); + await ctx.Request.Body.ReadAsync(buffer, 6, 5); await ctx.Response.WriteAsync(Encoding.UTF8.GetString(buffer)); } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs index 9225795d1c..46ce78701c 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs @@ -1933,6 +1933,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await InitializeConnectionAsync(async context => { + var bodyControlFeature = context.Features.Get(); + bodyControlFeature.AllowSynchronousIO = true; // Fill the flow control window to create async back pressure. await context.Response.Body.WriteAsync(new byte[windowSize + 1], 0, windowSize + 1); context.Response.Body.Write(new byte[1], 0, 1); @@ -2084,4 +2086,4 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); } } -} \ No newline at end of file +} diff --git a/src/Shared/RazorViews/BaseView.cs b/src/Shared/RazorViews/BaseView.cs index a171d8d1f2..3b661d7170 100644 --- a/src/Shared/RazorViews/BaseView.cs +++ b/src/Shared/RazorViews/BaseView.cs @@ -66,9 +66,13 @@ namespace Microsoft.Extensions.RazorViews Context = context; Request = Context.Request; Response = Context.Response; - Output = new StreamWriter(Response.Body, UTF8NoBOM, 4096, leaveOpen: true); + var buffer = new MemoryStream(); + Output = new StreamWriter(buffer, UTF8NoBOM, 4096, leaveOpen: true); await ExecuteAsync(); + await Output.FlushAsync(); Output.Dispose(); + buffer.Seek(0, SeekOrigin.Begin); + await buffer.CopyToAsync(Response.Body); } /// @@ -276,4 +280,4 @@ namespace Microsoft.Extensions.RazorViews .Select(HtmlEncoder.Encode)); } } -} \ No newline at end of file +}