Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AllowSynchronousIO to TestServer and IIS, fix tests #6404

Merged
merged 5 commits into from Jan 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
128 changes: 128 additions & 0 deletions 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<bool> _allowSynchronousIO;

internal AsyncStreamWrapper(Stream inner, Func<bool> allowSynchronousIO)
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
{
_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<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
return _inner.ReadAsync(buffer, offset, count, cancellationToken);
}

public override ValueTask<int> ReadAsync(Memory<byte> 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<byte> 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();
}
}
}
6 changes: 4 additions & 2 deletions src/Hosting/TestHost/src/ClientHandler.cs
Expand Up @@ -43,6 +43,8 @@ public ClientHandler(PathString pathBase, IHttpApplication<Context> application)
_pathBase = pathBase;
}

internal bool AllowSynchronousIO { get; set; }

/// <summary>
/// This adapts HttpRequestMessages to ASP.NET Core requests, dispatches them through the pipeline, and returns the
/// associated HttpResponseMessage.
Expand All @@ -59,7 +61,7 @@ public ClientHandler(PathString pathBase, IHttpApplication<Context> application)
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);
Expand Down Expand Up @@ -110,7 +112,7 @@ public ClientHandler(PathString pathBase, IHttpApplication<Context> application)
// 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;
});
Expand Down
14 changes: 9 additions & 5 deletions 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;
Expand All @@ -11,7 +11,7 @@

namespace Microsoft.AspNetCore.TestHost
{
internal class HttpContextBuilder
internal class HttpContextBuilder : IHttpBodyControlFeature
{
private readonly IHttpApplication<Context> _application;
private readonly HttpContext _httpContext;
Expand All @@ -23,24 +23,28 @@ internal class HttpContextBuilder
private bool _pipelineFinished;
private Context _testContext;

internal HttpContextBuilder(IHttpApplication<Context> application)
internal HttpContextBuilder(IHttpApplication<Context> 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<IHttpBodyControlFeature>(this);
_httpContext.Features.Set<IHttpResponseFeature>(_responseFeature);
var requestLifetimeFeature = new HttpRequestLifetimeFeature();
requestLifetimeFeature.RequestAborted = _requestAbortedSource.Token;
_httpContext.Features.Set<IHttpRequestLifetimeFeature>(requestLifetimeFeature);

_responseStream = new ResponseStream(ReturnResponseMessageAsync, AbortRequest);
_responseStream = new ResponseStream(ReturnResponseMessageAsync, AbortRequest, () => AllowSynchronousIO);
_responseFeature.Body = _responseStream;
}

public bool AllowSynchronousIO { get; set; }

internal void Configure(Action<HttpContext> configureContext)
{
if (configureContext == null)
Expand Down Expand Up @@ -136,4 +140,4 @@ internal void Abort(Exception exception)
_responseTcs.TrySetException(exception);
}
}
}
}
9 changes: 8 additions & 1 deletion src/Hosting/TestHost/src/ResponseStream.cs
Expand Up @@ -24,13 +24,15 @@ internal class ResponseStream : Stream
private Func<Task> _onFirstWriteAsync;
private bool _firstWrite;
private Action _abortRequest;
private Func<bool> _allowSynchronousIO;

private Pipe _pipe = new Pipe();

internal ResponseStream(Func<Task> onFirstWriteAsync, Action abortRequest)
internal ResponseStream(Func<Task> onFirstWriteAsync, Action abortRequest, Func<bool> 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);
}
Expand Down Expand Up @@ -144,6 +146,11 @@ private Task FirstWriteAsync()
// 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();
}
Expand Down
15 changes: 12 additions & 3 deletions src/Hosting/TestHost/src/TestServer.cs
Expand Up @@ -77,6 +77,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"/>
/// </summary>
/// <remarks>
/// Defaults to true.
/// </remarks>
public bool AllowSynchronousIO { get; set; } = true;
Tratcher marked this conversation as resolved.
Show resolved Hide resolved

private IHttpApplication<Context> Application
{
get => _application ?? throw new InvalidOperationException("The server has not been started or no web application was configured.");
Expand All @@ -85,7 +93,7 @@ private IHttpApplication<Context> Application
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()
Expand All @@ -96,7 +104,7 @@ public HttpClient CreateClient()
public WebSocketClient CreateWebSocketClient()
{
var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress);
return new WebSocketClient(pathBase, Application);
return new WebSocketClient(pathBase, Application) { AllowSynchronousIO = AllowSynchronousIO };
}

/// <summary>
Expand All @@ -120,7 +128,7 @@ public async Task<HttpContext> SendAsync(Action<HttpContext> configureContext, C
throw new ArgumentNullException(nameof(configureContext));
}

var builder = new HttpContextBuilder(Application);
var builder = new HttpContextBuilder(Application, AllowSynchronousIO);
builder.Configure(context =>
{
var request = context.Request;
Expand All @@ -138,6 +146,7 @@ public async Task<HttpContext> SendAsync(Action<HttpContext> configureContext, C
request.PathBase = pathBase;
});
builder.Configure(configureContext);
// TODO: Wrap the request body if any?
return await builder.SendAsync(cancellationToken).ConfigureAwait(false);
}

Expand Down
6 changes: 4 additions & 2 deletions src/Hosting/TestHost/src/WebSocketClient.cs
Expand Up @@ -46,10 +46,12 @@ public Action<HttpRequest> ConfigureRequest
set;
}

internal bool AllowSynchronousIO { get; set; }

public async Task<WebSocket> 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;
Expand Down Expand Up @@ -131,4 +133,4 @@ async Task<WebSocket> IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext c
}
}
}
}
}
5 changes: 2 additions & 3 deletions src/Hosting/TestHost/test/ClientHandlerTests.cs
Expand Up @@ -92,13 +92,12 @@ public Task SingleSlashNotMovedToPathBase()
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);
Expand Down
1 change: 1 addition & 0 deletions src/Hosting/TestHost/test/HttpContextBuilderTests.cs
Expand Up @@ -109,6 +109,7 @@ public async Task HeadersAvailableBeforeSyncBodyFinished()
{
c.Response.Headers["TestHeader"] = "TestValue";
var bytes = Encoding.UTF8.GetBytes("BodyStarted" + Environment.NewLine);
c.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
c.Response.Body.Write(bytes, 0, bytes.Length);
await block.Task;
bytes = Encoding.UTF8.GetBytes("BodyFinished");
Expand Down
13 changes: 6 additions & 7 deletions src/Hosting/TestHost/test/TestClientTests.cs
Expand Up @@ -87,8 +87,8 @@ public async Task SingleTrailingSlash_NoPathBase()
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();
Expand All @@ -106,7 +106,7 @@ public async Task PostAsyncWorks()
{
// 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();
Expand All @@ -132,16 +132,15 @@ public async Task LargePayload_DisposesRequest_AfterResponseIsCompleted()
}

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));
Expand Down