diff --git a/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs b/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs index eb8e7539c1..886f5c0052 100644 --- a/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs +++ b/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs @@ -161,10 +161,14 @@ namespace Microsoft.AspNetCore.WebUtilities public override System.Threading.Tasks.Task FlushAsync() { throw null; } public override void Write(char value) { } public override void Write(char[] values, int index, int count) { } + public override void Write(System.ReadOnlySpan value) { } public override void Write(string value) { } public override System.Threading.Tasks.Task WriteAsync(char value) { throw null; } public override System.Threading.Tasks.Task WriteAsync(char[] values, int index, int count) { throw null; } + public override System.Threading.Tasks.Task WriteAsync(System.ReadOnlyMemory value, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override System.Threading.Tasks.Task WriteAsync(string value) { throw null; } + public override void WriteLine(System.ReadOnlySpan value) { } + public override System.Threading.Tasks.Task WriteLineAsync(System.ReadOnlyMemory value, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public partial struct KeyValueAccumulator diff --git a/src/Http/WebUtilities/src/HttpResponseStreamWriter.cs b/src/Http/WebUtilities/src/HttpResponseStreamWriter.cs index a17116ae3e..4f16231e14 100644 --- a/src/Http/WebUtilities/src/HttpResponseStreamWriter.cs +++ b/src/Http/WebUtilities/src/HttpResponseStreamWriter.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.AspNetCore.WebUtilities @@ -127,6 +128,30 @@ namespace Microsoft.AspNetCore.WebUtilities } } + public override void Write(ReadOnlySpan value) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } + + var written = 0; + while (written < value.Length) + { + if (_charBufferCount == _charBufferSize) + { + FlushInternal(flushEncoder: false); + } + + written = CopyToCharBuffer(value); + + if (written < value.Length) + { + value = value.Slice(written); + } + }; + } + public override void Write(string value) { if (_disposed) @@ -152,6 +177,17 @@ namespace Microsoft.AspNetCore.WebUtilities } } + public override void WriteLine(ReadOnlySpan value) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } + + Write(value); + Write(NewLine); + } + public override Task WriteAsync(char value) { if (_disposed) @@ -268,6 +304,95 @@ namespace Microsoft.AspNetCore.WebUtilities } } + public override Task WriteAsync(ReadOnlyMemory value, CancellationToken cancellationToken = default) + { + if (_disposed) + { + return GetObjectDisposedTask(); + } + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + if (value.IsEmpty) + { + return Task.CompletedTask; + } + + var remaining = _charBufferSize - _charBufferCount; + if (remaining >= value.Length) + { + // Enough room in buffer, no need to go async + CopyToCharBuffer(value.Span); + return Task.CompletedTask; + } + else + { + return WriteAsyncAwaited(value); + } + } + + private async Task WriteAsyncAwaited(ReadOnlyMemory value) + { + Debug.Assert(value.Length > 0); + Debug.Assert(_charBufferSize - _charBufferCount < value.Length); + + int written = 0; + while (written < value.Length) + { + if (_charBufferCount == _charBufferSize) + { + await FlushInternalAsync(flushEncoder: false); + } + + written = CopyToCharBuffer(value.Span); + + if (written < value.Length) + { + value = value.Slice(written); + } + }; + } + + public override Task WriteLineAsync(ReadOnlyMemory value, CancellationToken cancellationToken = default) + { + if (_disposed) + { + return GetObjectDisposedTask(); + } + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + if (value.IsEmpty && NewLine.Length == 0) + { + return Task.CompletedTask; + } + + var remaining = _charBufferSize - _charBufferCount; + if (remaining >= value.Length + NewLine.Length) + { + // Enough room in buffer, no need to go async + CopyToCharBuffer(value.Span); + CopyToCharBuffer(NewLine); + return Task.CompletedTask; + } + else + { + return WriteLineAsyncAwaited(value); + } + } + + private async Task WriteLineAsyncAwaited(ReadOnlyMemory value) + { + await WriteAsyncAwaited(value); + await WriteAsyncAwaited(NewLine); + } + // We want to flush the stream when Flush/FlushAsync is explicitly // called by the user (example: from a Razor view). @@ -423,6 +548,19 @@ namespace Microsoft.AspNetCore.WebUtilities count -= remaining; } + private int CopyToCharBuffer(ReadOnlySpan value) + { + var remaining = Math.Min(_charBufferSize - _charBufferCount, value.Length); + + var source = value.Slice(0, remaining); + var destination = new Span(_charBuffer, _charBufferCount, remaining); + source.CopyTo(destination); + + _charBufferCount += remaining; + + return remaining; + } + [MethodImpl(MethodImplOptions.NoInlining)] private static Task GetObjectDisposedTask() { diff --git a/src/Http/WebUtilities/test/HttpResponseStreamWriterTest.cs b/src/Http/WebUtilities/test/HttpResponseStreamWriterTest.cs index 30402a5bbc..61437e0bdb 100644 --- a/src/Http/WebUtilities/test/HttpResponseStreamWriterTest.cs +++ b/src/Http/WebUtilities/test/HttpResponseStreamWriterTest.cs @@ -253,6 +253,62 @@ namespace Microsoft.AspNetCore.WebUtilities Assert.Equal(byteLength, stream.Length); } + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public void WriteReadOnlySpanChar_WritesToStream(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + using (writer) + { + var array = new string('a', byteLength).ToCharArray(); + var span = new ReadOnlySpan(array); + writer.Write(span); + } + + // Assert + Assert.Equal(byteLength, stream.Length); + } + + [Theory] + [InlineData(1022, "\n")] + [InlineData(1023, "\n")] + [InlineData(1024, "\n")] + [InlineData(1050, "\n")] + [InlineData(2047, "\n")] + [InlineData(2048, "\n")] + [InlineData(1021, "\r\n")] + [InlineData(1022, "\r\n")] + [InlineData(1023, "\r\n")] + [InlineData(1024, "\r\n")] + [InlineData(1050, "\r\n")] + [InlineData(2046, "\r\n")] + [InlineData(2048, "\r\n")] + public void WriteLineReadOnlySpanChar_WritesToStream(int byteLength, string newLine) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + writer.NewLine = newLine; + // Act + using (writer) + { + var array = new string('a', byteLength).ToCharArray(); + var span = new ReadOnlySpan(array); + writer.WriteLine(span); + } + + // Assert + Assert.Equal(byteLength + newLine.Length, stream.Length); + } + [Theory] [InlineData(1023)] [InlineData(1024)] @@ -298,6 +354,102 @@ namespace Microsoft.AspNetCore.WebUtilities Assert.Equal(byteLength, stream.Length); } + [Theory] + [InlineData(0)] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public async Task WriteReadOnlyMemoryAsync_WritesToStream(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + using (writer) + { + var array = new string('a', byteLength).ToCharArray(); + var memory = new ReadOnlyMemory(array); + await writer.WriteAsync(memory); + } + + // Assert + Assert.Equal(byteLength, stream.Length); + } + + [Fact] + public async Task WriteReadOnlyMemoryAsync_TokenCanceled_ReturnsCanceledTask() + { + // Arrange + var stream = new TestMemoryStream(); + using var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + var memory = new ReadOnlyMemory(new char[] { 'a' }); + var cancellationToken = new CancellationToken(true); + + // Act + await Assert.ThrowsAsync(async () => await writer.WriteAsync(memory, cancellationToken)); + + // Assert + Assert.Equal(0, stream.Length); + } + + [Theory] + [InlineData(0, 1)] + [InlineData(1022, 1)] + [InlineData(1023, 1)] + [InlineData(1024, 1)] + [InlineData(1050, 1)] + [InlineData(2047, 1)] + [InlineData(2048, 1)] + [InlineData(1021, 2)] + [InlineData(1022, 2)] + [InlineData(1023, 2)] + [InlineData(1024, 2)] + [InlineData(1024, 1023)] + [InlineData(1024, 1024)] + [InlineData(1024, 1050)] + [InlineData(1050, 2)] + [InlineData(2046, 2)] + [InlineData(2048, 2)] + public async Task WriteLineReadOnlyMemoryAsync_WritesToStream(int byteLength, int newLineLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + writer.NewLine = new string('\n', newLineLength); + + // Act + using (writer) + { + var array = new string('a', byteLength).ToCharArray(); + var memory = new ReadOnlyMemory(array); + await writer.WriteLineAsync(memory); + } + + // Assert + Assert.Equal(byteLength + newLineLength, stream.Length); + } + + [Fact] + public async Task WriteLineReadOnlyMemoryAsync_TokenCanceled_ReturnsCanceledTask() + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + var memory = new ReadOnlyMemory(new char[] { 'a' }); + var cancellationToken = new CancellationToken(true); + + // Act + using (writer) + { + await Assert.ThrowsAsync(async () => await writer.WriteLineAsync(memory, cancellationToken)); + } + + // Assert + Assert.Equal(0, stream.Length); + } + [Theory] [InlineData("你好世界", "utf-16")] [InlineData("హలో ప్రపంచ", "iso-8859-1")] @@ -539,11 +691,15 @@ namespace Microsoft.AspNetCore.WebUtilities { httpResponseStreamWriter.Write(new char[] { 'a', 'b' }, 0, 1); })}; - yield return new object[] { new Action((httpResponseStreamWriter) => { httpResponseStreamWriter.Write("hello"); })}; + yield return new object[] { new Action((httpResponseStreamWriter) => + { + httpResponseStreamWriter.Write(new ReadOnlySpan(new char[] { 'a', 'b' })); + })}; + yield return new object[] { new Action((httpResponseStreamWriter) => { httpResponseStreamWriter.Flush(); @@ -560,11 +716,19 @@ namespace Microsoft.AspNetCore.WebUtilities { await httpResponseStreamWriter.WriteAsync(new char[] { 'a', 'b' }, 0, 1); })}; - yield return new object[] { new Func(async (httpResponseStreamWriter) => { await httpResponseStreamWriter.WriteAsync("hello"); })}; + yield return new object[] { new Func(async (httpResponseStreamWriter) => + { + await httpResponseStreamWriter.WriteAsync(new ReadOnlyMemory(new char[] { 'a', 'b' })); + })}; + yield return new object[] { new Func(async (httpResponseStreamWriter) => + { + await httpResponseStreamWriter.WriteLineAsync(new ReadOnlyMemory(new char[] { 'a', 'b' })); + })}; + yield return new object[] { new Func(async (httpResponseStreamWriter) => { await httpResponseStreamWriter.FlushAsync();