diff --git a/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs b/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs index 4417d47e82..a40ca4a856 100644 --- a/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs +++ b/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs @@ -152,6 +152,8 @@ namespace Microsoft.AspNetCore.WebUtilities public override string ReadLine() { throw null; } [System.Diagnostics.DebuggerStepThroughAttribute] public override System.Threading.Tasks.Task ReadLineAsync() { throw null; } + [System.Diagnostics.DebuggerStepThroughAttribute] + public override System.Threading.Tasks.Task ReadToEndAsync() { throw null; } } public partial class HttpResponseStreamWriter : System.IO.TextWriter { diff --git a/src/Http/WebUtilities/src/HttpRequestStreamReader.cs b/src/Http/WebUtilities/src/HttpRequestStreamReader.cs index c500fa2978..be0fe7dae1 100644 --- a/src/Http/WebUtilities/src/HttpRequestStreamReader.cs +++ b/src/Http/WebUtilities/src/HttpRequestStreamReader.cs @@ -333,7 +333,7 @@ namespace Microsoft.AspNetCore.WebUtilities if (_disposed) { throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); - } + } StringBuilder sb = null; var consumeLineFeed = false; @@ -528,6 +528,20 @@ namespace Microsoft.AspNetCore.WebUtilities return _charsRead; } + public async override Task ReadToEndAsync() + { + StringBuilder sb = new StringBuilder(_charsRead - _charBufferIndex); + do + { + int tmpCharPos = _charBufferIndex; + sb.Append(_charBuffer, tmpCharPos, _charsRead - tmpCharPos); + _charBufferIndex = _charsRead; // We consumed these characters + await ReadIntoBufferAsync().ConfigureAwait(false); + } while (_charsRead > 0); + + return sb.ToString(); + } + private readonly struct ReadLineStepResult { public static readonly ReadLineStepResult Done = new ReadLineStepResult(true, null); diff --git a/src/Http/WebUtilities/test/HttpRequestStreamReaderTest.cs b/src/Http/WebUtilities/test/HttpRequestStreamReaderTest.cs index 5d65accbe0..e945906773 100644 --- a/src/Http/WebUtilities/test/HttpRequestStreamReaderTest.cs +++ b/src/Http/WebUtilities/test/HttpRequestStreamReaderTest.cs @@ -7,6 +7,7 @@ using System.Buffers; using System.Collections.Generic; using System.IO; using System.Text; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -55,6 +56,22 @@ namespace Microsoft.AspNetCore.WebUtilities Assert.Equal(5000, result.Length); } + [Fact] + public static async Task ReadToEndAsync_Reads_Asynchronously() + { + // Arrange + var stream = new AsyncOnlyStreamWrapper(GetLargeStream()); + var reader = new HttpRequestStreamReader(stream, Encoding.UTF8); + var streamReader = new StreamReader(GetLargeStream()); + string expected = await streamReader.ReadToEndAsync(); + + // Act + var actual = await reader.ReadToEndAsync(); + + // Assert + Assert.Equal(expected, actual); + } + [Fact] public static void TestRead() { @@ -477,5 +494,84 @@ namespace Microsoft.AspNetCore.WebUtilities httpRequestStreamReader.ReadLineAsync() )}; } + + private class AsyncOnlyStreamWrapper : Stream + { + private readonly Stream _inner; + + public AsyncOnlyStreamWrapper(Stream inner) + { + _inner = inner; + } + + 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() + { + throw SyncOperationForbiddenException(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _inner.FlushAsync(cancellationToken); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw SyncOperationForbiddenException(); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.ReadAsync(buffer, offset, count, cancellationToken); + } + + 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) + { + throw SyncOperationForbiddenException(); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.WriteAsync(buffer, offset, count, cancellationToken); + } + + protected override void Dispose(bool disposing) + { + _inner.Dispose(); + } + + public override ValueTask DisposeAsync() + { + return _inner.DisposeAsync(); + } + + private Exception SyncOperationForbiddenException() + { + return new InvalidOperationException("The stream cannot be accessed synchronously"); + } + } } }