Skip to content

Commit 1f892d7

Browse files
authored
Add AllowSynchronousIO to TestServer and IIS, fix tests (#6404)
1 parent 0defbf7 commit 1f892d7

File tree

30 files changed

+253
-84
lines changed

30 files changed

+253
-84
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.IO;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
9+
namespace Microsoft.AspNetCore.TestHost
10+
{
11+
internal class AsyncStreamWrapper : Stream
12+
{
13+
private Stream _inner;
14+
private Func<bool> _allowSynchronousIO;
15+
16+
internal AsyncStreamWrapper(Stream inner, Func<bool> allowSynchronousIO)
17+
{
18+
_inner = inner;
19+
_allowSynchronousIO = allowSynchronousIO;
20+
}
21+
22+
public override bool CanRead => _inner.CanRead;
23+
24+
public override bool CanSeek => _inner.CanSeek;
25+
26+
public override bool CanWrite => _inner.CanWrite;
27+
28+
public override long Length => _inner.Length;
29+
30+
public override long Position { get => _inner.Position; set => _inner.Position = value; }
31+
32+
public override void Flush()
33+
{
34+
// Not blocking Flush because things like StreamWriter.Dispose() always call it.
35+
_inner.Flush();
36+
}
37+
38+
public override Task FlushAsync(CancellationToken cancellationToken)
39+
{
40+
return _inner.FlushAsync(cancellationToken);
41+
}
42+
43+
public override int Read(byte[] buffer, int offset, int count)
44+
{
45+
if (!_allowSynchronousIO())
46+
{
47+
throw new InvalidOperationException("Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true.");
48+
}
49+
50+
return _inner.Read(buffer, offset, count);
51+
}
52+
53+
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
54+
{
55+
return _inner.ReadAsync(buffer, offset, count, cancellationToken);
56+
}
57+
58+
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
59+
{
60+
return _inner.ReadAsync(buffer, cancellationToken);
61+
}
62+
63+
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
64+
{
65+
return _inner.BeginRead(buffer, offset, count, callback, state);
66+
}
67+
68+
public override int EndRead(IAsyncResult asyncResult)
69+
{
70+
return _inner.EndRead(asyncResult);
71+
}
72+
73+
public override long Seek(long offset, SeekOrigin origin)
74+
{
75+
return _inner.Seek(offset, origin);
76+
}
77+
78+
public override void SetLength(long value)
79+
{
80+
_inner.SetLength(value);
81+
}
82+
83+
public override void Write(byte[] buffer, int offset, int count)
84+
{
85+
if (!_allowSynchronousIO())
86+
{
87+
throw new InvalidOperationException("Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true.");
88+
}
89+
90+
_inner.Write(buffer, offset, count);
91+
}
92+
93+
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
94+
{
95+
return _inner.BeginWrite(buffer, offset, count, callback, state);
96+
}
97+
98+
public override void EndWrite(IAsyncResult asyncResult)
99+
{
100+
_inner.EndWrite(asyncResult);
101+
}
102+
103+
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
104+
{
105+
return _inner.WriteAsync(buffer, offset, count, cancellationToken);
106+
}
107+
108+
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
109+
{
110+
return _inner.WriteAsync(buffer, cancellationToken);
111+
}
112+
113+
public override void Close()
114+
{
115+
_inner.Close();
116+
}
117+
118+
protected override void Dispose(bool disposing)
119+
{
120+
_inner.Dispose();
121+
}
122+
123+
public override ValueTask DisposeAsync()
124+
{
125+
return _inner.DisposeAsync();
126+
}
127+
}
128+
}

src/Hosting/TestHost/src/ClientHandler.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ public ClientHandler(PathString pathBase, IHttpApplication<Context> application)
4343
_pathBase = pathBase;
4444
}
4545

46+
internal bool AllowSynchronousIO { get; set; }
47+
4648
/// <summary>
4749
/// This adapts HttpRequestMessages to ASP.NET Core requests, dispatches them through the pipeline, and returns the
4850
/// associated HttpResponseMessage.
@@ -59,7 +61,7 @@ protected override async Task<HttpResponseMessage> SendAsync(
5961
throw new ArgumentNullException(nameof(request));
6062
}
6163

62-
var contextBuilder = new HttpContextBuilder(_application);
64+
var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO);
6365

6466
Stream responseBody = null;
6567
var requestContent = request.Content ?? new StreamContent(Stream.Null);
@@ -110,7 +112,7 @@ protected override async Task<HttpResponseMessage> SendAsync(
110112
// This body may have been consumed before, rewind it.
111113
body.Seek(0, SeekOrigin.Begin);
112114
}
113-
req.Body = body;
115+
req.Body = new AsyncStreamWrapper(body, () => contextBuilder.AllowSynchronousIO);
114116

115117
responseBody = context.Response.Body;
116118
});

src/Hosting/TestHost/src/HttpContextBuilder.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -11,7 +11,7 @@
1111

1212
namespace Microsoft.AspNetCore.TestHost
1313
{
14-
internal class HttpContextBuilder
14+
internal class HttpContextBuilder : IHttpBodyControlFeature
1515
{
1616
private readonly IHttpApplication<Context> _application;
1717
private readonly HttpContext _httpContext;
@@ -23,24 +23,28 @@ internal class HttpContextBuilder
2323
private bool _pipelineFinished;
2424
private Context _testContext;
2525

26-
internal HttpContextBuilder(IHttpApplication<Context> application)
26+
internal HttpContextBuilder(IHttpApplication<Context> application, bool allowSynchronousIO)
2727
{
2828
_application = application ?? throw new ArgumentNullException(nameof(application));
29+
AllowSynchronousIO = allowSynchronousIO;
2930
_httpContext = new DefaultHttpContext();
3031

3132
var request = _httpContext.Request;
3233
request.Protocol = "HTTP/1.1";
3334
request.Method = HttpMethods.Get;
3435

36+
_httpContext.Features.Set<IHttpBodyControlFeature>(this);
3537
_httpContext.Features.Set<IHttpResponseFeature>(_responseFeature);
3638
var requestLifetimeFeature = new HttpRequestLifetimeFeature();
3739
requestLifetimeFeature.RequestAborted = _requestAbortedSource.Token;
3840
_httpContext.Features.Set<IHttpRequestLifetimeFeature>(requestLifetimeFeature);
3941

40-
_responseStream = new ResponseStream(ReturnResponseMessageAsync, AbortRequest);
42+
_responseStream = new ResponseStream(ReturnResponseMessageAsync, AbortRequest, () => AllowSynchronousIO);
4143
_responseFeature.Body = _responseStream;
4244
}
4345

46+
public bool AllowSynchronousIO { get; set; }
47+
4448
internal void Configure(Action<HttpContext> configureContext)
4549
{
4650
if (configureContext == null)
@@ -136,4 +140,4 @@ internal void Abort(Exception exception)
136140
_responseTcs.TrySetException(exception);
137141
}
138142
}
139-
}
143+
}

src/Hosting/TestHost/src/ResponseStream.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ internal class ResponseStream : Stream
2424
private Func<Task> _onFirstWriteAsync;
2525
private bool _firstWrite;
2626
private Action _abortRequest;
27+
private Func<bool> _allowSynchronousIO;
2728

2829
private Pipe _pipe = new Pipe();
2930

30-
internal ResponseStream(Func<Task> onFirstWriteAsync, Action abortRequest)
31+
internal ResponseStream(Func<Task> onFirstWriteAsync, Action abortRequest, Func<bool> allowSynchronousIO)
3132
{
3233
_onFirstWriteAsync = onFirstWriteAsync ?? throw new ArgumentNullException(nameof(onFirstWriteAsync));
3334
_abortRequest = abortRequest ?? throw new ArgumentNullException(nameof(abortRequest));
35+
_allowSynchronousIO = allowSynchronousIO ?? throw new ArgumentNullException(nameof(allowSynchronousIO));
3436
_firstWrite = true;
3537
_writeLock = new SemaphoreSlim(1, 1);
3638
}
@@ -144,6 +146,11 @@ private Task FirstWriteAsync()
144146
// Write with count 0 will still trigger OnFirstWrite
145147
public override void Write(byte[] buffer, int offset, int count)
146148
{
149+
if (!_allowSynchronousIO())
150+
{
151+
throw new InvalidOperationException("Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true.");
152+
}
153+
147154
// The Pipe Write method requires calling FlushAsync to notify the reader. Call WriteAsync instead.
148155
WriteAsync(buffer, offset, count).GetAwaiter().GetResult();
149156
}

src/Hosting/TestHost/src/TestServer.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ public IWebHost Host
7777

7878
public IFeatureCollection Features { get; }
7979

80+
/// <summary>
81+
/// Gets or sets a value that controls whether synchronous IO is allowed for the <see cref="HttpContext.Request"/> and <see cref="HttpContext.Response"/>
82+
/// </summary>
83+
/// <remarks>
84+
/// Defaults to true.
85+
/// </remarks>
86+
public bool AllowSynchronousIO { get; set; } = true;
87+
8088
private IHttpApplication<Context> Application
8189
{
8290
get => _application ?? throw new InvalidOperationException("The server has not been started or no web application was configured.");
@@ -85,7 +93,7 @@ private IHttpApplication<Context> Application
8593
public HttpMessageHandler CreateHandler()
8694
{
8795
var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress);
88-
return new ClientHandler(pathBase, Application);
96+
return new ClientHandler(pathBase, Application) { AllowSynchronousIO = AllowSynchronousIO };
8997
}
9098

9199
public HttpClient CreateClient()
@@ -96,7 +104,7 @@ public HttpClient CreateClient()
96104
public WebSocketClient CreateWebSocketClient()
97105
{
98106
var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress);
99-
return new WebSocketClient(pathBase, Application);
107+
return new WebSocketClient(pathBase, Application) { AllowSynchronousIO = AllowSynchronousIO };
100108
}
101109

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

123-
var builder = new HttpContextBuilder(Application);
131+
var builder = new HttpContextBuilder(Application, AllowSynchronousIO);
124132
builder.Configure(context =>
125133
{
126134
var request = context.Request;
@@ -138,6 +146,7 @@ public async Task<HttpContext> SendAsync(Action<HttpContext> configureContext, C
138146
request.PathBase = pathBase;
139147
});
140148
builder.Configure(configureContext);
149+
// TODO: Wrap the request body if any?
141150
return await builder.SendAsync(cancellationToken).ConfigureAwait(false);
142151
}
143152

src/Hosting/TestHost/src/WebSocketClient.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,12 @@ public Action<HttpRequest> ConfigureRequest
4646
set;
4747
}
4848

49+
internal bool AllowSynchronousIO { get; set; }
50+
4951
public async Task<WebSocket> ConnectAsync(Uri uri, CancellationToken cancellationToken)
5052
{
5153
WebSocketFeature webSocketFeature = null;
52-
var contextBuilder = new HttpContextBuilder(_application);
54+
var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO);
5355
contextBuilder.Configure(context =>
5456
{
5557
var request = context.Request;
@@ -131,4 +133,4 @@ async Task<WebSocket> IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext c
131133
}
132134
}
133135
}
134-
}
136+
}

src/Hosting/TestHost/test/ClientHandlerTests.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,12 @@ public Task SingleSlashNotMovedToPathBase()
9292
public async Task ResubmitRequestWorks()
9393
{
9494
int requestCount = 1;
95-
var handler = new ClientHandler(PathString.Empty, new DummyApplication(context =>
95+
var handler = new ClientHandler(PathString.Empty, new DummyApplication(async context =>
9696
{
97-
int read = context.Request.Body.Read(new byte[100], 0, 100);
97+
int read = await context.Request.Body.ReadAsync(new byte[100], 0, 100);
9898
Assert.Equal(11, read);
9999

100100
context.Response.Headers["TestHeader"] = "TestValue:" + requestCount++;
101-
return Task.FromResult(0);
102101
}));
103102

104103
HttpMessageInvoker invoker = new HttpMessageInvoker(handler);

src/Hosting/TestHost/test/HttpContextBuilderTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ public async Task HeadersAvailableBeforeSyncBodyFinished()
109109
{
110110
c.Response.Headers["TestHeader"] = "TestValue";
111111
var bytes = Encoding.UTF8.GetBytes("BodyStarted" + Environment.NewLine);
112+
c.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
112113
c.Response.Body.Write(bytes, 0, bytes.Length);
113114
await block.Task;
114115
bytes = Encoding.UTF8.GetBytes("BodyFinished");

src/Hosting/TestHost/test/TestClientTests.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ public async Task SingleTrailingSlash_NoPathBase()
8787
public async Task PutAsyncWorks()
8888
{
8989
// Arrange
90-
RequestDelegate appDelegate = ctx =>
91-
ctx.Response.WriteAsync(new StreamReader(ctx.Request.Body).ReadToEnd() + " PUT Response");
90+
RequestDelegate appDelegate = async ctx =>
91+
await ctx.Response.WriteAsync(await new StreamReader(ctx.Request.Body).ReadToEndAsync() + " PUT Response");
9292
var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate));
9393
var server = new TestServer(builder);
9494
var client = server.CreateClient();
@@ -106,7 +106,7 @@ public async Task PostAsyncWorks()
106106
{
107107
// Arrange
108108
RequestDelegate appDelegate = async ctx =>
109-
await ctx.Response.WriteAsync(new StreamReader(ctx.Request.Body).ReadToEnd() + " POST Response");
109+
await ctx.Response.WriteAsync(await new StreamReader(ctx.Request.Body).ReadToEndAsync() + " POST Response");
110110
var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate));
111111
var server = new TestServer(builder);
112112
var client = server.CreateClient();
@@ -132,16 +132,15 @@ public async Task LargePayload_DisposesRequest_AfterResponseIsCompleted()
132132
}
133133

134134
var builder = new WebHostBuilder();
135-
RequestDelegate app = (ctx) =>
135+
RequestDelegate app = async ctx =>
136136
{
137137
var disposable = new TestDisposable();
138138
ctx.Response.RegisterForDispose(disposable);
139-
ctx.Response.Body.Write(data, 0, 1024);
139+
await ctx.Response.Body.WriteAsync(data, 0, 1024);
140140

141141
Assert.False(disposable.IsDisposed);
142142

143-
ctx.Response.Body.Write(data, 1024, 1024);
144-
return Task.FromResult(0);
143+
await ctx.Response.Body.WriteAsync(data, 1024, 1024);
145144
};
146145

147146
builder.Configure(appBuilder => appBuilder.Run(app));

0 commit comments

Comments
 (0)