Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1f1bfe1
First API proposal implementation
ViveliDuCh Apr 8, 2026
f7a06f0
API replacements in production code
ViveliDuCh Feb 12, 2026
bd5cf98
Implementation update based on latest API Review final consensus
ViveliDuCh Apr 8, 2026
fec7a59
Address PR feedback. MemoryStream base change. Spillover buffer for C…
ViveliDuCh Apr 9, 2026
341ad57
Address PR feedback
ViveliDuCh Apr 14, 2026
8882b05
Merge branch 'dotnet:main' into stream-wrappers-api
ViveliDuCh May 8, 2026
0fb2ef5
Merge branch 'dotnet:main' into stream-wrappers-api
ViveliDuCh May 22, 2026
111aa77
Address PR feedback: fix StringStream flush offset, WritableMemoryStr…
ViveliDuCh May 29, 2026
430515a
Merge branch 'dotnet:main' into stream-wrappers-api
ViveliDuCh Jun 15, 2026
0946bfc
Address PR feedback: remove redundant code, improve test coverage and…
ViveliDuCh Jun 16, 2026
d5103d1
Merge remote-tracking branch
ViveliDuCh Jun 16, 2026
8e4a441
Address PR feedback: gap-zeroing, disposal checks, cancellation guard…
ViveliDuCh Jun 17, 2026
70f9f8b
Cache ReadAsync Task<int> in ReadOnly/Writable MemoryStream wrappers
ViveliDuCh Jun 18, 2026
242dbd5
Address feedback: perf fast paths, CopyTo overrides, sync-exception b…
ViveliDuCh Jun 19, 2026
af11328
Address review: BCL Seek exception parity, StringStream overflow guar…
ViveliDuCh Jun 19, 2026
b0882fd
Address PR feedback: drop StringStream CopyToAsync, share native-memo…
ViveliDuCh Jun 23, 2026
7096bcc
Address PR feedback: Expose MemoryStream fields as private protected …
ViveliDuCh Jun 23, 2026
6b69c9d
Address Copilot review: conformance null path and ref/impl mismatches
ViveliDuCh Jun 23, 2026
f69919e
Merge commit '6b69c9d0547a32796971ae85e6dea584f6961dce' into stream-w…
ViveliDuCh Jun 23, 2026
08abd54
Address PR feedback: tighten MemoryStream field visibility, simplify …
ViveliDuCh Jun 23, 2026
88a66b8
Address PR feedback
ViveliDuCh Jun 23, 2026
93611d0
Promote MemoryStream _isOpen/_lastReadTask to private protected; wrap…
ViveliDuCh Jun 23, 2026
7c4d0cc
Address PR feedback
ViveliDuCh Jun 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/libraries/Common/src/System/IO/ReadOnlyMemoryStream.cs
Comment thread
ViveliDuCh marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

// On net11.0+, the public ReadOnlyMemoryStream in System.Runtime (CoreLib) supersedes this internal copy.
#if !NET11_0_OR_GREATER
Comment thread
ViveliDuCh marked this conversation as resolved.

using System.Threading;
using System.Threading.Tasks;

Expand Down Expand Up @@ -213,3 +216,5 @@ private static void ValidateBufferArguments(byte[] buffer, int offset, int count
#endif
}
}

#endif // !NET11_0_OR_GREATER
Comment thread
ViveliDuCh marked this conversation as resolved.
File renamed without changes.
4 changes: 2 additions & 2 deletions src/libraries/System.Memory.Data/tests/BinaryDataTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ public void ValidatesSeekArguments()
Assert.Throws<IOException>(() => stream.Seek(-1, SeekOrigin.Begin));

Assert.Throws<ArgumentOutOfRangeException>(() => stream.Seek((long)int.MaxValue + 1, SeekOrigin.Begin));
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Seek(0, (SeekOrigin)3));
Assert.ThrowsAny<ArgumentException>(() => stream.Seek(0, (SeekOrigin)3));
}


Expand Down Expand Up @@ -715,7 +715,7 @@ public async Task CloseStreamValidation()
byte[] buffer = "some data"u8.ToArray();
Stream stream = new BinaryData(buffer).ToStream();
stream.Dispose();
Assert.Throws<ObjectDisposedException>(() => stream.Position = -1);
Assert.Throws<ObjectDisposedException>(() => stream.Position = 0);
Assert.Throws<ObjectDisposedException>(() => stream.Position);
Assert.Throws<ObjectDisposedException>(() => stream.Seek(0, SeekOrigin.Begin));
Assert.Throws<ObjectDisposedException>(() => stream.Read(buffer, 0, buffer.Length));
Expand Down
28 changes: 28 additions & 0 deletions src/libraries/System.Memory/ref/System.Memory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,34 @@ public void Rewind(long count) { }
public bool TryReadExact(int count, out System.Buffers.ReadOnlySequence<T> sequence) { throw null; }
}
}
namespace System.Buffers
{
public sealed partial class ReadOnlySequenceStream : System.IO.Stream
{
public ReadOnlySequenceStream(System.Buffers.ReadOnlySequence<byte> source) { }
public override bool CanRead { get { throw null; } }
public override bool CanSeek { get { throw null; } }
public override bool CanWrite { get { throw null; } }
public override long Length { get { throw null; } }
public override long Position { get { throw null; } set { } }
public override void CopyTo(System.IO.Stream destination, int bufferSize) { }
public override System.Threading.Tasks.Task CopyToAsync(System.IO.Stream destination, int bufferSize, System.Threading.CancellationToken cancellationToken) { throw null; }
protected override void Dispose(bool disposing) { }
public override void Flush() { }
public override System.Threading.Tasks.Task FlushAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
public override int Read(byte[] buffer, int offset, int count) { throw null; }
public override int Read(System.Span<byte> buffer) { throw null; }
public override System.Threading.Tasks.Task<int> ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; }
public override System.Threading.Tasks.ValueTask<int> ReadAsync(System.Memory<byte> buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public override int ReadByte() { throw null; }
Comment thread
ViveliDuCh marked this conversation as resolved.
public override long Seek(long offset, System.IO.SeekOrigin origin) { throw null; }
public override void SetLength(long value) { }
public override void Write(byte[] buffer, int offset, int count) { }
Comment thread
ViveliDuCh marked this conversation as resolved.
public override void Write(System.ReadOnlySpan<byte> buffer) { }
public override System.Threading.Tasks.Task WriteAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; }
public override System.Threading.Tasks.ValueTask WriteAsync(System.ReadOnlyMemory<byte> buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
}
}
namespace System.Runtime.InteropServices
{
public static partial class SequenceMarshal
Expand Down
9 changes: 9 additions & 0 deletions src/libraries/System.Memory/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,13 @@
<data name="BufferMaximumSizeExceeded" xml:space="preserve">
<value>Cannot allocate a buffer of size {0}.</value>
</data>
<data name="NotSupported_UnwritableStream" xml:space="preserve">
<value>Stream does not support writing.</value>
</data>
<data name="IO_SeekBeforeBegin" xml:space="preserve">
<value>An attempt was made to move the position before the beginning of the stream.</value>
</data>
<data name="Argument_InvalidSeekOrigin" xml:space="preserve">
<value>Invalid seek origin.</value>
</data>
</root>
3 changes: 3 additions & 0 deletions src/libraries/System.Memory/src/System.Memory.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<Compile Include="System\Buffers\ReadOnlySequence.cs" />
<Compile Include="System\Buffers\ReadOnlySequenceDebugView.cs" />
<Compile Include="System\Buffers\ReadOnlySequenceSegment.cs" />
<Compile Include="System\Buffers\ReadOnlySequenceStream.cs" />
<Compile Include="System\Buffers\ReadOnlySequence.Helpers.cs" />
<Compile Include="System\Buffers\SequenceReader.cs" />
<Compile Include="System\Buffers\SequenceReader.Search.cs" />
Expand All @@ -40,6 +41,8 @@
<!-- Common or Common-branched source files -->
<Compile Include="$(CommonPath)System\Buffers\ArrayBufferWriter.cs"
Link="Common\System\Buffers\ArrayBufferWriter.cs" />
<Compile Include="$(CommonPath)System\Threading\Tasks\CachedCompletedInt32Task.cs"
Link="Common\System\Threading\Tasks\CachedCompletedInt32Task.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace System.Buffers
{
/// <summary>
/// Provides a seekable, read-only <see cref="Stream"/> over a <see cref="ReadOnlySequence{Byte}"/>.
/// </summary>
/// <remarks>
/// <para>The underlying sequence is not copied; reads are served directly from its segments.</para>
/// <para>The stream cannot be written to. <see cref="CanWrite"/> always returns <see langword="false"/>.</para>
/// </remarks>
public sealed class ReadOnlySequenceStream : Stream
Comment thread
ViveliDuCh marked this conversation as resolved.
{
private ReadOnlySequence<byte> _sequence;
private SequencePosition _position;
private long _absolutePosition;
private bool _isDisposed;
private CachedCompletedInt32Task _lastReadTask;

/// <summary>
/// Initializes a new instance of the <see cref="ReadOnlySequenceStream"/> class over the specified <see cref="ReadOnlySequence{Byte}"/>.
/// </summary>
/// <param name="source">The <see cref="ReadOnlySequence{Byte}"/> to wrap.</param>
public ReadOnlySequenceStream(ReadOnlySequence<byte> source)
{
_sequence = source;
_position = source.Start;
_absolutePosition = 0;
_isDisposed = false;
}

/// <inheritdoc />
public override bool CanRead => !_isDisposed;

/// <inheritdoc />
public override bool CanSeek => !_isDisposed;

/// <inheritdoc />
public override bool CanWrite => false;

private void EnsureNotDisposed() => ObjectDisposedException.ThrowIf(_isDisposed, this);

/// <inheritdoc />
public override long Length
{
get
{
EnsureNotDisposed();
return _sequence.Length;
}
}

/// <inheritdoc />
public override long Position
{
get
{
EnsureNotDisposed();
return _absolutePosition;
}
set
Comment thread
ViveliDuCh marked this conversation as resolved.
{
EnsureNotDisposed();
ArgumentOutOfRangeException.ThrowIfNegative(value);

if (value >= _sequence.Length)
{
_position = _sequence.End;
}
else if (value >= _absolutePosition)
{
_position = _sequence.GetPosition(value - _absolutePosition, _position);
}
else
{
_position = _sequence.GetPosition(value, _sequence.Start);
}

_absolutePosition = value;
}
}

/// <inheritdoc />
public override int Read(byte[] buffer, int offset, int count)
{
ValidateBufferArguments(buffer, offset, count);
return Read(buffer.AsSpan(offset, count));
}

/// <inheritdoc />
public override int Read(Span<byte> buffer)
Comment thread
ViveliDuCh marked this conversation as resolved.
{
EnsureNotDisposed();

if (_absolutePosition >= _sequence.Length)
{
return 0;
}

ReadOnlySequence<byte> remaining = _sequence.Slice(_position);
int n = (int)Math.Min(remaining.Length, buffer.Length);
if (n <= 0)
{
return 0;
}

remaining.Slice(0, n).CopyTo(buffer);
_position = _sequence.GetPosition(n, _position);
_absolutePosition += n;
return n;
}

/// <inheritdoc />
public override int ReadByte()
{
EnsureNotDisposed();

byte b = 0;
return Read(new Span<byte>(ref b)) > 0 ? b : -1;
}

Comment thread
ViveliDuCh marked this conversation as resolved.
/// <inheritdoc/>
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
ValidateBufferArguments(buffer, offset, count);
EnsureNotDisposed();

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

int n = Read(buffer, offset, count);
return _lastReadTask.GetTask(n);
}

/// <inheritdoc/>
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
EnsureNotDisposed();

if (cancellationToken.IsCancellationRequested)
{
return ValueTask.FromCanceled<int>(cancellationToken);
}

int n = Read(buffer.Span);
return new ValueTask<int>(n);
}

/// <inheritdoc />
public override void CopyTo(Stream destination, int bufferSize)
{
ValidateCopyToArguments(destination, bufferSize);
EnsureNotDisposed();

if (_absolutePosition >= _sequence.Length)
{
return;
}

ReadOnlySequence<byte> remaining = _sequence.Slice(_position);
foreach (ReadOnlyMemory<byte> segment in remaining)
{
destination.Write(segment.Span);
}

_position = _sequence.End;
_absolutePosition = _sequence.Length;
}

/// <inheritdoc />
public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
{
ValidateCopyToArguments(destination, bufferSize);
EnsureNotDisposed();

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

if (_absolutePosition >= _sequence.Length)
{
return Task.CompletedTask;
}

return CopyToAsyncCore(destination, cancellationToken);
}

private async Task CopyToAsyncCore(Stream destination, CancellationToken cancellationToken)
Comment thread
ViveliDuCh marked this conversation as resolved.
{
ReadOnlySequence<byte> remaining = _sequence.Slice(_position);
foreach (ReadOnlyMemory<byte> segment in remaining)
{
await destination.WriteAsync(segment, cancellationToken).ConfigureAwait(false);
}

_position = _sequence.End;
_absolutePosition = _sequence.Length;
}

/// <inheritdoc />
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(SR.NotSupported_UnwritableStream);

/// <inheritdoc/>
public override void Write(ReadOnlySpan<byte> buffer) => throw new NotSupportedException(SR.NotSupported_UnwritableStream);

/// <inheritdoc/>
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new NotSupportedException(SR.NotSupported_UnwritableStream);

/// <inheritdoc/>
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) => throw new NotSupportedException(SR.NotSupported_UnwritableStream);

/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin)
{
EnsureNotDisposed();

long basePosition = origin switch
{
SeekOrigin.Begin => 0L,
SeekOrigin.Current => _absolutePosition,
SeekOrigin.End => _sequence.Length,
_ => throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin))
};

ArgumentOutOfRangeException.ThrowIfGreaterThan(offset, long.MaxValue - basePosition);

long absolutePosition = basePosition + offset;

if (absolutePosition < 0)
{
throw new IOException(SR.IO_SeekBeforeBegin);
}
Comment thread
ViveliDuCh marked this conversation as resolved.

if (absolutePosition >= _sequence.Length)
{
_position = _sequence.End;
}
else if (absolutePosition >= _absolutePosition)
{
_position = _sequence.GetPosition(absolutePosition - _absolutePosition, _position);
}
else
{
_position = _sequence.GetPosition(absolutePosition, _sequence.Start);
Comment thread
ViveliDuCh marked this conversation as resolved.
}

_absolutePosition = absolutePosition;
return absolutePosition;
}

/// <inheritdoc />
public override void Flush() { }
Comment thread
ViveliDuCh marked this conversation as resolved.

/// <inheritdoc />
public override Task FlushAsync(CancellationToken cancellationToken) =>
cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask;

/// <inheritdoc />
public override void SetLength(long value) => throw new NotSupportedException(SR.NotSupported_UnwritableStream);

/// <inheritdoc />
protected override void Dispose(bool disposing)
{
_isDisposed = true;
_sequence = default;
base.Dispose(disposing);
Comment thread
ViveliDuCh marked this conversation as resolved.
}
}
}
Loading
Loading