Skip to content

Added Write/WriteLine ReadOnlySpan/ReadOnlyMemory overrides to HttpResponseStreamWriter #18451

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

Merged
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,14 @@ public override void Flush() { }
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<char> values) { }
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<char> values, 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<char> values) { }
public override System.Threading.Tasks.Task WriteLineAsync(System.ReadOnlyMemory<char> values, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
}
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
public partial struct KeyValueAccumulator
Expand Down
138 changes: 138 additions & 0 deletions src/Http/WebUtilities/src/HttpResponseStreamWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.AspNetCore.WebUtilities
Expand Down Expand Up @@ -127,6 +128,30 @@ public override void Write(char[] values, int index, int count)
}
}

public override void Write(ReadOnlySpan<char> values)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(HttpResponseStreamWriter));
}

int written = 0;
while (written < values.Length)
{
if (_charBufferCount == _charBufferSize)
{
FlushInternal(flushEncoder: false);
}

written = CopyToCharBuffer(values);

if (written < values.Length)
{
values = values.Slice(written);
}
};
}

public override void Write(string value)
{
if (_disposed)
Expand All @@ -152,6 +177,17 @@ public override void Write(string value)
}
}

public override void WriteLine(ReadOnlySpan<char> values)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(HttpResponseStreamWriter));
}

Write(values);
Write(NewLine);
}

public override Task WriteAsync(char value)
{
if (_disposed)
Expand Down Expand Up @@ -268,6 +304,95 @@ private async Task WriteAsyncAwaited(string value)
}
}

public override Task WriteAsync(ReadOnlyMemory<char> values, CancellationToken cancellationToken = default)
{
if (_disposed)
{
return GetObjectDisposedTask();
}

if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}

if (values.IsEmpty)
{
return Task.CompletedTask;
}

var remaining = _charBufferSize - _charBufferCount;
if (remaining >= values.Length)
{
// Enough room in buffer, no need to go async
CopyToCharBuffer(values.Span);
return Task.CompletedTask;
}
else
{
return WriteAsyncAwaited(values);
}
}

private async Task WriteAsyncAwaited(ReadOnlyMemory<char> values)
{
Debug.Assert(values.Length > 0);
Debug.Assert(_charBufferSize - _charBufferCount < values.Length);

int written = 0;
while (written < values.Length)
{
if (_charBufferCount == _charBufferSize)
{
await FlushInternalAsync(flushEncoder: false);
}

written = CopyToCharBuffer(values.Span);

if (written < values.Length)
{
values = values.Slice(written);
}
};
}

public override Task WriteLineAsync(ReadOnlyMemory<char> values, CancellationToken cancellationToken = default)
{
if (_disposed)
{
return GetObjectDisposedTask();
}

if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}

if (values.IsEmpty && NewLine.Length == 0)
{
return Task.CompletedTask;
}

var remaining = _charBufferSize - _charBufferCount;
if (remaining >= values.Length + NewLine.Length)
{
// Enough room in buffer, no need to go async
CopyToCharBuffer(values.Span);
CopyToCharBuffer(NewLine);
return Task.CompletedTask;
}
else
{
return WriteLineAsyncAwaited(values);
}
}

private async Task WriteLineAsyncAwaited(ReadOnlyMemory<char> values)
{
await WriteAsyncAwaited(values);
await WriteAsyncAwaited(NewLine);
}

// We want to flush the stream when Flush/FlushAsync is explicitly
// called by the user (example: from a Razor view).

Expand Down Expand Up @@ -423,6 +548,19 @@ private void CopyToCharBuffer(char[] values, ref int index, ref int count)
count -= remaining;
}

private int CopyToCharBuffer(ReadOnlySpan<char> values)
{
var remaining = Math.Min(_charBufferSize - _charBufferCount, values.Length);

var source = values.Slice(0, remaining);
var destination = new Span<char>(_charBuffer, _charBufferCount, remaining);
source.CopyTo(destination);

_charBufferCount += remaining;

return remaining;
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static Task GetObjectDisposedTask()
{
Expand Down
168 changes: 166 additions & 2 deletions src/Http/WebUtilities/test/HttpResponseStreamWriterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,62 @@ public void WriteCharArray_WritesToStream(int byteLength)
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<char>(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<char>(array);
writer.WriteLine(span);
}

// Assert
Assert.Equal(byteLength + newLine.Length, stream.Length);
}

[Theory]
[InlineData(1023)]
[InlineData(1024)]
Expand Down Expand Up @@ -298,6 +354,102 @@ public async Task WriteCharArrayAsync_WritesToStream(int byteLength)
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<char>(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<char>(new char[] { 'a' });
var cancellationToken = new CancellationToken(true);

// Act
await Assert.ThrowsAsync<TaskCanceledException>(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<char>(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<char>(new char[] { 'a' });
var cancellationToken = new CancellationToken(true);

// Act
using (writer)
{
await Assert.ThrowsAsync<TaskCanceledException>(async () => await writer.WriteLineAsync(memory, cancellationToken));
}

// Assert
Assert.Equal(0, stream.Length);
}

[Theory]
[InlineData("你好世界", "utf-16")]
[InlineData("హలో ప్రపంచ", "iso-8859-1")]
Expand Down Expand Up @@ -539,11 +691,15 @@ public static IEnumerable<object[]> HttpResponseDisposeData()
{
httpResponseStreamWriter.Write(new char[] { 'a', 'b' }, 0, 1);
})};

yield return new object[] { new Action<HttpResponseStreamWriter>((httpResponseStreamWriter) =>
{
httpResponseStreamWriter.Write("hello");
})};
yield return new object[] { new Action<HttpResponseStreamWriter>((httpResponseStreamWriter) =>
{
httpResponseStreamWriter.Write(new ReadOnlySpan<char>(new char[] { 'a', 'b' }));
})};

yield return new object[] { new Action<HttpResponseStreamWriter>((httpResponseStreamWriter) =>
{
httpResponseStreamWriter.Flush();
Expand All @@ -560,11 +716,19 @@ public static IEnumerable<object[]> HttpResponseDisposeDataAsync()
{
await httpResponseStreamWriter.WriteAsync(new char[] { 'a', 'b' }, 0, 1);
})};

yield return new object[] { new Func<HttpResponseStreamWriter, Task>(async (httpResponseStreamWriter) =>
{
await httpResponseStreamWriter.WriteAsync("hello");
})};
yield return new object[] { new Func<HttpResponseStreamWriter, Task>(async (httpResponseStreamWriter) =>
{
await httpResponseStreamWriter.WriteAsync(new ReadOnlyMemory<char>(new char[] { 'a', 'b' }));
})};
yield return new object[] { new Func<HttpResponseStreamWriter, Task>(async (httpResponseStreamWriter) =>
{
await httpResponseStreamWriter.WriteLineAsync(new ReadOnlyMemory<char>(new char[] { 'a', 'b' }));
})};

yield return new object[] { new Func<HttpResponseStreamWriter, Task>(async (httpResponseStreamWriter) =>
{
await httpResponseStreamWriter.FlushAsync();
Expand Down