Skip to content

Add Stream wrappers for memory and text-based types#126669

Open
ViveliDuCh wants to merge 12 commits into
dotnet:mainfrom
ViveliDuCh:stream-wrappers-api
Open

Add Stream wrappers for memory and text-based types#126669
ViveliDuCh wants to merge 12 commits into
dotnet:mainfrom
ViveliDuCh:stream-wrappers-api

Conversation

@ViveliDuCh

@ViveliDuCh ViveliDuCh commented Apr 8, 2026

Copy link
Copy Markdown
Member

Fixes #82801

Introduces standardized stream wrappers for text and memory types as public sealed classes (StringStream, ReadOnlyMemoryStream, WritableMemoryStream, and ReadOnlySequenceStream ) as approved in API review (video).

What's included

  • StringStream (System.IO, CoreLib): non-seekable, read-only stream that encodes string or ReadOnlyMemory<char> on-the-fly. Encoding is required (no default). Never emits BOMs.
  • ReadOnlyMemoryStream (System.IO, CoreLib): seekable, read-only stream over ReadOnlyMemory<byte>
  • WritableMemoryStream (System.IO, CoreLib): seekable, read-write stream over Memory<byte> with fixed capacity
  • ReadOnlySequenceStream (System.Buffers, System.Memory): seekable, read-only stream over ReadOnlySequence<byte>
  • Ref assembly updates for System.Runtime and System.Memory
  • Updated 3 internal call sites (ReadOnlyMemoryContent, XmlPreloadedResolver, JsonXmlDataContract) to use the new types
  • Conformance and behavioral tests for all 4 types

Implementation notes

  • All types are public sealed with direct constructors: the review moved away from factory methods on Stream to avoid derived-type IntelliSense noise
  • StringStream encodes directly into the caller's buffer in Read() rather than using an internal byte buffer
  • ReadOnlyMemoryStream and WritableMemoryStream derive from MemoryStream.
  • TryGetBuffer returns false and GetBuffer throws on the memory stream types (no publiclyVisible support for now)

Limitations

  • The internal shared-source ReadOnlyMemoryStream in Common/ cannot be fully replaced:
    • System.Memory.Data multi-targets netstandard2.0 where the new public type doesn't exist.
    • System.Net.Http (net11.0 only) can adopt it directly.

Follow-up work

  • IBufferWriter<byte>Stream wrapper (separate proposal per #100434)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces new standardized Stream wrapper types across CoreLib and System.Memory, aligning with the approved API shape from #82801 and updating a few internal call sites to use the new wrappers.

Changes:

  • Added new public sealed stream wrappers: StringStream, ReadOnlyMemoryStream, WritableMemoryStream (CoreLib) and ReadOnlySequenceStream (System.Memory).
  • Updated reference assemblies and project files to expose/compile the new APIs.
  • Added conformance + targeted behavioral tests, and updated select internal consumers to use StringStream.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs Implements the new StringStream API (read-only, non-seekable, on-the-fly encoding).
src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs Implements seekable read-only stream over ReadOnlyMemory<byte>.
src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs Implements seekable fixed-capacity read/write stream over Memory<byte>.
src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems Wires new CoreLib stream wrapper source files into the build.
src/libraries/System.Private.CoreLib/src/Resources/Strings.resx Adds a new resource string entry (currently appears unused).
src/libraries/System.Runtime/ref/System.Runtime.cs Updates public API surface to include the new stream wrapper types (and a small ref signature normalization change).
src/libraries/System.Runtime/tests/System.IO.Tests/System.IO.Tests.csproj Includes the new test files in the System.IO test project.
src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamConformanceTests.cs Adds conformance coverage for StringStream (string + ReadOnlyMemory<char> overloads).
src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs Adds targeted StringStream(string, Encoding) behavioral tests.
src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs Adds targeted StringStream(ReadOnlyMemory<char>, Encoding) behavioral tests.
src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamConformanceTests.cs Adds conformance coverage for ReadOnlyMemoryStream.
src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs Adds targeted ReadOnlyMemoryStream behavioral tests.
src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs Adds conformance coverage for WritableMemoryStream (with some overridden tests).
src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs Adds targeted WritableMemoryStream behavioral tests.
src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs Implements ReadOnlySequenceStream over ReadOnlySequence<byte>.
src/libraries/System.Memory/src/System.Memory.csproj Includes the new ReadOnlySequenceStream source file in System.Memory.
src/libraries/System.Memory/src/Resources/Strings.resx Adds SR strings needed for stream-like exception messages in System.Memory.
src/libraries/System.Memory/ref/System.Memory.cs Adds ReadOnlySequenceStream to the System.Memory ref surface.
src/libraries/System.Memory/tests/System.Memory.Tests.csproj Adds tests for ReadOnlySequenceStream and references StreamConformanceTests.
src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStreamTests.cs Adds targeted behavioral tests for multi-segment ReadOnlySequenceStream scenarios.
src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStream.ConformanceTests.cs Adds conformance coverage for ReadOnlySequenceStream.
src/libraries/System.Private.Xml/src/System/Xml/Resolvers/XmlPreloadedResolver.cs Switches internal string-to-stream conversion to StringStream.
src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs Switches internal string-to-stream conversion to StringStream.
Comments suppressed due to low confidence (1)

src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs:38

  • Variable is still named 'memoryStream' but the implementation is now StringStream (not a MemoryStream). Rename the local to avoid implying the stream is seekable/in-memory-buffered (e.g., 'xmlContentStream').
            Stream memoryStream = new StringStream(xmlContent, Encoding.UTF8);
            object? xmlValue;
            XmlDictionaryReaderQuotas? quotas = ((JsonReaderDelegator)jsonReader).ReaderQuotas;
            if (quotas == null)
            {
                xmlValue = dataContractSerializer.ReadObject(memoryStream);

Comment thread src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs
Comment thread src/libraries/System.Runtime/ref/System.Runtime.cs
Comment thread src/libraries/System.Private.CoreLib/src/Resources/Strings.resx Outdated
Comment thread src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs Outdated
Comment thread src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs
Comment thread src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs
Comment thread src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs Outdated
Comment thread src/libraries/System.Memory/ref/System.Memory.cs
Copilot AI review requested due to automatic review settings April 9, 2026 08:01
@ViveliDuCh ViveliDuCh force-pushed the stream-wrappers-api branch from 27761ff to b43698b Compare April 9, 2026 08:01
@ViveliDuCh ViveliDuCh force-pushed the stream-wrappers-api branch from b43698b to 95a2989 Compare April 9, 2026 08:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated 10 comments.

Comment thread src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs
Comment thread src/libraries/System.Runtime/tests/System.IO.Tests/System.IO.Tests.csproj Outdated
Comment thread src/libraries/System.Memory/src/System.Memory.csproj Outdated
Comment thread src/libraries/System.Memory/tests/System.Memory.Tests.csproj Outdated
Copilot AI review requested due to automatic review settings April 13, 2026 19:23
@ViveliDuCh ViveliDuCh force-pushed the stream-wrappers-api branch from 5e3f98c to fec7a59 Compare April 13, 2026 19:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated 3 comments.

Comment thread src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs Outdated
Comment thread src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated 8 comments.

Comment thread src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs
Comment thread src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs Outdated
Comment thread src/libraries/System.Memory/ref/System.Memory.cs Outdated
Comment on lines +199 to +209
Task<int> task1 = stream.ReadAsync(buffer1, 0, 5);
Task<int> task2 = stream.ReadAsync(buffer2, 0, 5);
Task<int> task3 = stream.ReadAsync(buffer3, 0, 5);

await task1;
await task2;
await task3;

Assert.Same(task1, task2);
Assert.Same(task2, task3);

@ViveliDuCh ViveliDuCh May 29, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional — validates the Task caching optimization built into the stream implementation.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may want to reconsider this one, I don't see the Task caching optimization built into ReadOnlySequenceStream.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks. Moved CachedCompletedInt32Task to Common/src/ as shared source (since the struct is internal to CoreLib and inaccessible from System.Memory) and wired it into ReadOnlySequenceStream.ReadAsync(byte[], ...) via a _lastReadTask field, matching the established MemoryStream/BufferedStream pattern.

Comment thread src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs Outdated
ViveliDuCh and others added 2 commits May 22, 2026 14:50
Copilot AI review requested due to automatic review settings May 29, 2026 15:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated 3 comments.

@adamsitnik adamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the simplicity of the design and implementation.

Overall it LGTM, I found mostly some nits related to tests. I will need to perform another round for StringStream.

Thank you @ViveliDuCh !

Comment thread src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStreamTests.cs Outdated
Comment thread src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStreamTests.cs Outdated
Comment thread src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs Outdated
@ViveliDuCh

Copy link
Copy Markdown
Member Author

Note

AI-generated content (Copilot)

Base Class Benchmark: MemoryStream vs Stream

Investigation: Comparing sealed subclass performance when deriving from MemoryStream (current API review decision) vs Stream directly.

Both variants have identical method bodies — the only difference is the base class. Categories measured:

  • Construction — object size difference (~81 vs ~37 bytes), base constructor field init
  • ReadByte — per-byte hot loop (concrete & polymorphic dispatch)
  • ReadSpan — 4096-byte chunked reads
  • CopyTo — optimized single-span write
  • WriteByte / WriteSpan — write hot paths
  • BinaryReader — integration through BinaryReader wrapper

Per the investigation doc and benchmark spec, the prediction is that only construction will show a measurable difference; all operations should be within noise (±2-3%) since both are sealed with identical devirtualization behavior.

@EgorBot -linux_amd -osx_arm64

using System;
using System.IO;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(ReadOnlyStreamBenchmarks).Assembly).Run(args);

// ═══════════════════════════════════════════════════════════════════════════════
// Benchmark: ReadOnlyMemoryStream — MemoryStream base vs Stream base
// ═══════════════════════════════════════════════════════════════════════════════

[MemoryDiagnoser]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
public class ReadOnlyStreamBenchmarks
{
    private byte[] _data = default!;
    private ReadOnlyMemory<byte> _memory;

    [Params(1024, 65536)]
    public int Size { get; set; }

    [GlobalSetup]
    public void Setup()
    {
        _data = new byte[Size];
        new Random(42).NextBytes(_data);
        _memory = _data.AsMemory();
    }

    // ── Construction ──

    [BenchmarkCategory("Construction"), Benchmark(Baseline = true)]
    public object Construct_MemoryStreamBase() => new ReadOnlyMemoryStreamMS(_memory);

    [BenchmarkCategory("Construction"), Benchmark]
    public object Construct_StreamBase() => new ReadOnlyMemoryStreamS(_memory);

    [BenchmarkCategory("Construction"), Benchmark]
    public object Construct_ByteArrayBaseline() => new MemoryStream(_data, writable: false);

    // ── ReadByte (concrete type — devirtualized) ──

    [BenchmarkCategory("ReadByte"), Benchmark(Baseline = true)]
    public int ReadByte_MemoryStreamBase()
    {
        var s = new ReadOnlyMemoryStreamMS(_memory);
        int count = 0;
        while (s.ReadByte() != -1) count++;
        return count;
    }

    [BenchmarkCategory("ReadByte"), Benchmark]
    public int ReadByte_StreamBase()
    {
        var s = new ReadOnlyMemoryStreamS(_memory);
        int count = 0;
        while (s.ReadByte() != -1) count++;
        return count;
    }

    [BenchmarkCategory("ReadByte"), Benchmark]
    public int ReadByte_ByteArrayBaseline()
    {
        var s = new MemoryStream(_data, writable: false);
        int count = 0;
        while (s.ReadByte() != -1) count++;
        return count;
    }

    // ── ReadByte via Stream reference (polymorphic dispatch) ──

    [BenchmarkCategory("ReadByte_Polymorphic"), Benchmark(Baseline = true)]
    public int ReadBytePoly_MemoryStreamBase()
    {
        Stream s = new ReadOnlyMemoryStreamMS(_memory);
        int count = 0;
        while (s.ReadByte() != -1) count++;
        return count;
    }

    [BenchmarkCategory("ReadByte_Polymorphic"), Benchmark]
    public int ReadBytePoly_StreamBase()
    {
        Stream s = new ReadOnlyMemoryStreamS(_memory);
        int count = 0;
        while (s.ReadByte() != -1) count++;
        return count;
    }

    // ── ReadSpan (chunked) ──

    [BenchmarkCategory("ReadSpan"), Benchmark(Baseline = true)]
    public int ReadSpan_MemoryStreamBase()
    {
        var s = new ReadOnlyMemoryStreamMS(_memory);
        Span<byte> buf = stackalloc byte[4096];
        int total = 0, n;
        while ((n = s.Read(buf)) > 0) total += n;
        return total;
    }

    [BenchmarkCategory("ReadSpan"), Benchmark]
    public int ReadSpan_StreamBase()
    {
        var s = new ReadOnlyMemoryStreamS(_memory);
        Span<byte> buf = stackalloc byte[4096];
        int total = 0, n;
        while ((n = s.Read(buf)) > 0) total += n;
        return total;
    }

    [BenchmarkCategory("ReadSpan"), Benchmark]
    public int ReadSpan_ByteArrayBaseline()
    {
        var s = new MemoryStream(_data, writable: false);
        Span<byte> buf = stackalloc byte[4096];
        int total = 0, n;
        while ((n = s.Read(buf)) > 0) total += n;
        return total;
    }

    // ── CopyTo ──

    [BenchmarkCategory("CopyTo"), Benchmark(Baseline = true)]
    public void CopyTo_MemoryStreamBase()
    {
        var s = new ReadOnlyMemoryStreamMS(_memory);
        s.CopyTo(Stream.Null);
    }

    [BenchmarkCategory("CopyTo"), Benchmark]
    public void CopyTo_StreamBase()
    {
        var s = new ReadOnlyMemoryStreamS(_memory);
        s.CopyTo(Stream.Null);
    }

    [BenchmarkCategory("CopyTo"), Benchmark]
    public void CopyTo_ByteArrayBaseline()
    {
        var s = new MemoryStream(_data, writable: false);
        s.CopyTo(Stream.Null);
    }

    // ── BinaryReader integration ──

    [BenchmarkCategory("BinaryReader"), Benchmark(Baseline = true)]
    public int BinaryReader_MemoryStreamBase()
    {
        var s = new ReadOnlyMemoryStreamMS(_memory);
        using var br = new BinaryReader(s);
        int count = 0;
        for (int i = 0; i < Size; i++) { br.ReadByte(); count++; }
        return count;
    }

    [BenchmarkCategory("BinaryReader"), Benchmark]
    public int BinaryReader_StreamBase()
    {
        var s = new ReadOnlyMemoryStreamS(_memory);
        using var br = new BinaryReader(s);
        int count = 0;
        for (int i = 0; i < Size; i++) { br.ReadByte(); count++; }
        return count;
    }

    [BenchmarkCategory("BinaryReader"), Benchmark]
    public int BinaryReader_ByteArrayBaseline()
    {
        var s = new MemoryStream(_data, writable: false);
        using var br = new BinaryReader(s);
        int count = 0;
        for (int i = 0; i < Size; i++) { br.ReadByte(); count++; }
        return count;
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// Benchmark: WritableMemoryStream — MemoryStream base vs Stream base
// ═══════════════════════════════════════════════════════════════════════════════

[MemoryDiagnoser]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
public class WritableStreamBenchmarks
{
    private byte[] _data = default!;

    [Params(1024, 65536)]
    public int Size { get; set; }

    [GlobalSetup]
    public void Setup()
    {
        _data = new byte[Size];
        new Random(42).NextBytes(_data);
    }

    // ── Construction ──

    [BenchmarkCategory("Construction"), Benchmark(Baseline = true)]
    public object Construct_MemoryStreamBase() => new WritableMemoryStreamMS(new byte[Size]);

    [BenchmarkCategory("Construction"), Benchmark]
    public object Construct_StreamBase() => new WritableMemoryStreamS(new byte[Size]);

    // ── WriteByte ──

    [BenchmarkCategory("WriteByte"), Benchmark(Baseline = true)]
    public void WriteByte_MemoryStreamBase()
    {
        var s = new WritableMemoryStreamMS(new byte[Size]);
        for (int i = 0; i < Size; i++) s.WriteByte((byte)i);
    }

    [BenchmarkCategory("WriteByte"), Benchmark]
    public void WriteByte_StreamBase()
    {
        var s = new WritableMemoryStreamS(new byte[Size]);
        for (int i = 0; i < Size; i++) s.WriteByte((byte)i);
    }

    [BenchmarkCategory("WriteByte"), Benchmark]
    public void WriteByte_ByteArrayBaseline()
    {
        var s = new MemoryStream(new byte[Size], writable: true);
        for (int i = 0; i < Size; i++) s.WriteByte((byte)i);
    }

    // ── WriteSpan ──

    [BenchmarkCategory("WriteSpan"), Benchmark(Baseline = true)]
    public void WriteSpan_MemoryStreamBase()
    {
        var s = new WritableMemoryStreamMS(new byte[Size]);
        ReadOnlySpan<byte> chunk = _data.AsSpan(0, Math.Min(256, Size));
        for (int i = 0; i < Size / Math.Max(chunk.Length, 1); i++) s.Write(chunk);
    }

    [BenchmarkCategory("WriteSpan"), Benchmark]
    public void WriteSpan_StreamBase()
    {
        var s = new WritableMemoryStreamS(new byte[Size]);
        ReadOnlySpan<byte> chunk = _data.AsSpan(0, Math.Min(256, Size));
        for (int i = 0; i < Size / Math.Max(chunk.Length, 1); i++) s.Write(chunk);
    }

    [BenchmarkCategory("WriteSpan"), Benchmark]
    public void WriteSpan_ByteArrayBaseline()
    {
        var s = new MemoryStream(new byte[Size], writable: true);
        ReadOnlySpan<byte> chunk = _data.AsSpan(0, Math.Min(256, Size));
        for (int i = 0; i < Size / Math.Max(chunk.Length, 1); i++) s.Write(chunk);
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// Variant A: ReadOnlyMemoryStream : MemoryStream (current API review decision)
// Source: PR #126669 — sealed subclass, all virtuals overridden, independent state
// ═══════════════════════════════════════════════════════════════════════════════

public class ReadOnlyMemoryStreamMS : MemoryStream
{
    private ReadOnlyMemory<byte> _buffer;
    private int _position;
    private bool _isOpen;

    public ReadOnlyMemoryStreamMS(ReadOnlyMemory<byte> source) : base()
    {
        _buffer = source;
        _isOpen = true;
    }

    public override bool CanRead => _isOpen;
    public override bool CanSeek => _isOpen;
    public override bool CanWrite => false;
    public override int Capacity { get => _buffer.Length; set => throw new NotSupportedException(); }
    public override long Length { get { ThrowClosed(); return _buffer.Length; } }

    public override long Position
    {
        get { ThrowClosed(); return _position; }
        set
        {
            ThrowClosed();
            ArgumentOutOfRangeException.ThrowIfNegative(value);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(value, int.MaxValue);
            _position = (int)value;
        }
    }

    public override int ReadByte()
    {
        ThrowClosed();
        ReadOnlySpan<byte> span = _buffer.Span;
        int pos = _position;
        if ((uint)pos < (uint)span.Length) { _position++; return span[pos]; }
        return -1;
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        ValidateBufferArguments(buffer, offset, count);
        return Read(new Span<byte>(buffer, offset, count));
    }

    public override int Read(Span<byte> buffer)
    {
        ThrowClosed();
        int remaining = _buffer.Length - _position;
        if (remaining <= 0 || buffer.Length == 0) return 0;
        int n = Math.Min(remaining, buffer.Length);
        _buffer.Span.Slice(_position, n).CopyTo(buffer);
        _position += n;
        return n;
    }

    public override void CopyTo(Stream destination, int bufferSize)
    {
        ValidateCopyToArguments(destination, bufferSize);
        ThrowClosed();
        if (_buffer.Length > _position)
        {
            destination.Write(_buffer.Span.Slice(_position));
            _position = _buffer.Length;
        }
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        ThrowClosed();
        long newPos = origin switch
        {
            SeekOrigin.Begin => offset,
            SeekOrigin.Current => _position + offset,
            SeekOrigin.End => _buffer.Length + offset,
            _ => throw new ArgumentException("Invalid seek origin")
        };
        if (newPos < 0) throw new IOException("Seek before begin");
        _position = (int)newPos;
        return newPos;
    }

    public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
    public override void Write(ReadOnlySpan<byte> buffer) => throw new NotSupportedException();
    public override void WriteByte(byte value) => throw new NotSupportedException();
    public override void SetLength(long value) => throw new NotSupportedException();
    public override void Flush() { }

    public override byte[] GetBuffer() => throw new UnauthorizedAccessException();
    public override bool TryGetBuffer(out ArraySegment<byte> buffer) { buffer = default; return false; }
    public override byte[] ToArray()
    {
        ThrowClosed();
        if (_buffer.Length == 0) return Array.Empty<byte>();
        byte[] copy = GC.AllocateUninitializedArray<byte>(_buffer.Length);
        _buffer.Span.CopyTo(copy);
        return copy;
    }

    public override void WriteTo(Stream stream) { ArgumentNullException.ThrowIfNull(stream); ThrowClosed(); if (_buffer.Length > 0) stream.Write(_buffer.Span); }

    protected override void Dispose(bool disposing) { _isOpen = false; _buffer = default; base.Dispose(disposing); }
    private void ThrowClosed() => ObjectDisposedException.ThrowIf(!_isOpen, this);
}

// ═══════════════════════════════════════════════════════════════════════════════
// Variant B: ReadOnlyMemoryStream : Stream (alternative — identical method bodies)
// ═══════════════════════════════════════════════════════════════════════════════

public class ReadOnlyMemoryStreamS : Stream
{
    private ReadOnlyMemory<byte> _buffer;
    private int _position;
    private bool _isOpen;

    public ReadOnlyMemoryStreamS(ReadOnlyMemory<byte> source)
    {
        _buffer = source;
        _isOpen = true;
    }

    public override bool CanRead => _isOpen;
    public override bool CanSeek => _isOpen;
    public override bool CanWrite => false;
    public override long Length { get { ThrowClosed(); return _buffer.Length; } }

    public override long Position
    {
        get { ThrowClosed(); return _position; }
        set
        {
            ThrowClosed();
            ArgumentOutOfRangeException.ThrowIfNegative(value);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(value, int.MaxValue);
            _position = (int)value;
        }
    }

    public override int ReadByte()
    {
        ThrowClosed();
        ReadOnlySpan<byte> span = _buffer.Span;
        int pos = _position;
        if ((uint)pos < (uint)span.Length) { _position++; return span[pos]; }
        return -1;
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        ValidateBufferArguments(buffer, offset, count);
        return Read(new Span<byte>(buffer, offset, count));
    }

    public override int Read(Span<byte> buffer)
    {
        ThrowClosed();
        int remaining = _buffer.Length - _position;
        if (remaining <= 0 || buffer.Length == 0) return 0;
        int n = Math.Min(remaining, buffer.Length);
        _buffer.Span.Slice(_position, n).CopyTo(buffer);
        _position += n;
        return n;
    }

    public override void CopyTo(Stream destination, int bufferSize)
    {
        ValidateCopyToArguments(destination, bufferSize);
        ThrowClosed();
        if (_buffer.Length > _position)
        {
            destination.Write(_buffer.Span.Slice(_position));
            _position = _buffer.Length;
        }
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        ThrowClosed();
        long newPos = origin switch
        {
            SeekOrigin.Begin => offset,
            SeekOrigin.Current => _position + offset,
            SeekOrigin.End => _buffer.Length + offset,
            _ => throw new ArgumentException("Invalid seek origin")
        };
        if (newPos < 0) throw new IOException("Seek before begin");
        _position = (int)newPos;
        return newPos;
    }

    public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
    public override void Write(ReadOnlySpan<byte> buffer) => throw new NotSupportedException();
    public override void WriteByte(byte value) => throw new NotSupportedException();
    public override void SetLength(long value) => throw new NotSupportedException();
    public override void Flush() { }

    protected override void Dispose(bool disposing) { _isOpen = false; _buffer = default; base.Dispose(disposing); }
    private void ThrowClosed() => ObjectDisposedException.ThrowIf(!_isOpen, this);
}

// ═══════════════════════════════════════════════════════════════════════════════
// Variant A: WritableMemoryStream : MemoryStream
// ═══════════════════════════════════════════════════════════════════════════════

public class WritableMemoryStreamMS : MemoryStream
{
    private Memory<byte> _buffer;
    private int _position;
    private int _length;
    private bool _isOpen;

    public WritableMemoryStreamMS(Memory<byte> buffer) : base()
    {
        _buffer = buffer;
        _length = 0;
        _isOpen = true;
    }

    public override bool CanRead => _isOpen;
    public override bool CanSeek => _isOpen;
    public override bool CanWrite => _isOpen;
    public override int Capacity { get => _buffer.Length; set => throw new NotSupportedException(); }
    public override long Length { get { ThrowClosed(); return _length; } }

    public override long Position
    {
        get { ThrowClosed(); return _position; }
        set
        {
            ThrowClosed();
            ArgumentOutOfRangeException.ThrowIfNegative(value);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(value, int.MaxValue);
            _position = (int)value;
        }
    }

    public override int ReadByte()
    {
        ThrowClosed();
        ReadOnlySpan<byte> span = _buffer.Span;
        int pos = _position;
        if ((uint)pos < (uint)_length) { _position++; return span[pos]; }
        return -1;
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        ValidateBufferArguments(buffer, offset, count);
        return Read(new Span<byte>(buffer, offset, count));
    }

    public override int Read(Span<byte> buffer)
    {
        ThrowClosed();
        int remaining = _length - _position;
        if (remaining <= 0 || buffer.Length == 0) return 0;
        int n = Math.Min(remaining, buffer.Length);
        ((ReadOnlyMemory<byte>)_buffer).Span.Slice(_position, n).CopyTo(buffer);
        _position += n;
        return n;
    }

    public override void WriteByte(byte value)
    {
        ThrowClosed();
        if (_position >= _buffer.Length) throw new NotSupportedException();
        _buffer.Span[_position++] = value;
        if (_position > _length) _length = _position;
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        ValidateBufferArguments(buffer, offset, count);
        Write(new ReadOnlySpan<byte>(buffer, offset, count));
    }

    public override void Write(ReadOnlySpan<byte> buffer)
    {
        ThrowClosed();
        if (buffer.Length == 0) return;
        if (_position > _buffer.Length - buffer.Length) throw new NotSupportedException();
        buffer.CopyTo(_buffer.Span.Slice(_position));
        _position += buffer.Length;
        if (_position > _length) _length = _position;
    }

    public override void CopyTo(Stream destination, int bufferSize)
    {
        ValidateCopyToArguments(destination, bufferSize);
        ThrowClosed();
        if (_length > _position)
        {
            destination.Write(((ReadOnlyMemory<byte>)_buffer).Span.Slice(_position, _length - _position));
            _position = _length;
        }
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        ThrowClosed();
        long newPos = origin switch
        {
            SeekOrigin.Begin => offset,
            SeekOrigin.Current => _position + offset,
            SeekOrigin.End => _length + offset,
            _ => throw new ArgumentException("Invalid seek origin")
        };
        if (newPos < 0) throw new IOException("Seek before begin");
        _position = (int)newPos;
        return newPos;
    }

    public override void SetLength(long value) => throw new NotSupportedException();
    public override void Flush() { }

    public override byte[] GetBuffer() => throw new UnauthorizedAccessException();
    public override bool TryGetBuffer(out ArraySegment<byte> buffer) { buffer = default; return false; }
    public override byte[] ToArray()
    {
        ThrowClosed();
        if (_length == 0) return Array.Empty<byte>();
        byte[] copy = GC.AllocateUninitializedArray<byte>(_length);
        ((ReadOnlyMemory<byte>)_buffer).Span.Slice(0, _length).CopyTo(copy);
        return copy;
    }

    public override void WriteTo(Stream stream) { ArgumentNullException.ThrowIfNull(stream); ThrowClosed(); if (_length > 0) stream.Write(((ReadOnlyMemory<byte>)_buffer).Span.Slice(0, _length)); }

    protected override void Dispose(bool disposing) { _isOpen = false; _buffer = default; base.Dispose(disposing); }
    private void ThrowClosed() => ObjectDisposedException.ThrowIf(!_isOpen, this);
}

// ═══════════════════════════════════════════════════════════════════════════════
// Variant B: WritableMemoryStream : Stream
// ═══════════════════════════════════════════════════════════════════════════════

public class WritableMemoryStreamS : Stream
{
    private Memory<byte> _buffer;
    private int _position;
    private int _length;
    private bool _isOpen;

    public WritableMemoryStreamS(Memory<byte> buffer)
    {
        _buffer = buffer;
        _length = 0;
        _isOpen = true;
    }

    public override bool CanRead => _isOpen;
    public override bool CanSeek => _isOpen;
    public override bool CanWrite => _isOpen;
    public override long Length { get { ThrowClosed(); return _length; } }

    public override long Position
    {
        get { ThrowClosed(); return _position; }
        set
        {
            ThrowClosed();
            ArgumentOutOfRangeException.ThrowIfNegative(value);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(value, int.MaxValue);
            _position = (int)value;
        }
    }

    public override int ReadByte()
    {
        ThrowClosed();
        ReadOnlySpan<byte> span = _buffer.Span;
        int pos = _position;
        if ((uint)pos < (uint)_length) { _position++; return span[pos]; }
        return -1;
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        ValidateBufferArguments(buffer, offset, count);
        return Read(new Span<byte>(buffer, offset, count));
    }

    public override int Read(Span<byte> buffer)
    {
        ThrowClosed();
        int remaining = _length - _position;
        if (remaining <= 0 || buffer.Length == 0) return 0;
        int n = Math.Min(remaining, buffer.Length);
        ((ReadOnlyMemory<byte>)_buffer).Span.Slice(_position, n).CopyTo(buffer);
        _position += n;
        return n;
    }

    public override void WriteByte(byte value)
    {
        ThrowClosed();
        if (_position >= _buffer.Length) throw new NotSupportedException();
        _buffer.Span[_position++] = value;
        if (_position > _length) _length = _position;
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        ValidateBufferArguments(buffer, offset, count);
        Write(new ReadOnlySpan<byte>(buffer, offset, count));
    }

    public override void Write(ReadOnlySpan<byte> buffer)
    {
        ThrowClosed();
        if (buffer.Length == 0) return;
        if (_position > _buffer.Length - buffer.Length) throw new NotSupportedException();
        buffer.CopyTo(_buffer.Span.Slice(_position));
        _position += buffer.Length;
        if (_position > _length) _length = _position;
    }

    public override void CopyTo(Stream destination, int bufferSize)
    {
        ValidateCopyToArguments(destination, bufferSize);
        ThrowClosed();
        if (_length > _position)
        {
            destination.Write(((ReadOnlyMemory<byte>)_buffer).Span.Slice(_position, _length - _position));
            _position = _length;
        }
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        ThrowClosed();
        long newPos = origin switch
        {
            SeekOrigin.Begin => offset,
            SeekOrigin.Current => _position + offset,
            SeekOrigin.End => _length + offset,
            _ => throw new ArgumentException("Invalid seek origin")
        };
        if (newPos < 0) throw new IOException("Seek before begin");
        _position = (int)newPos;
        return newPos;
    }

    public override void SetLength(long value) => throw new NotSupportedException();
    public override void Flush() { }

    protected override void Dispose(bool disposing) { _isOpen = false; _buffer = default; base.Dispose(disposing); }
    private void ThrowClosed() => ObjectDisposedException.ThrowIf(!_isOpen, this);
}

@ViveliDuCh

Copy link
Copy Markdown
Member Author

Note

AI-generated content (Copilot)

Base Class Benchmark: WritableMemoryStreamMemoryStream vs Stream

Companion to the ReadOnlyMemoryStream benchmark above. Per the API review decision, both ReadOnlyMemoryStream and WritableMemoryStream derive from MemoryStream.

This benchmark covers the writable path with identical method bodies, differing only in base class. Categories:

  • Construction — object size (~85 vs ~41 bytes for writable, extra _length field)
  • WriteByte — per-byte write hot loop (concrete type)
  • WriteByte_Polymorphic — per-byte write via Stream reference
  • WriteSpan — 256-byte chunked writes
  • ReadByteAfterWrite — fill buffer then read back per-byte
  • CopyToAfterWrite — fill buffer then CopyTo
  • BinaryReader — fill buffer then read through BinaryReader

@EgorBot -linux_amd -osx_arm64

using System;
using System.IO;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(WritableStreamBenchmarks).Assembly).Run(args);

// ═══════════════════════════════════════════════════════════════════════════════
// Benchmark: WritableMemoryStream — MemoryStream base vs Stream base
// ═══════════════════════════════════════════════════════════════════════════════

[MemoryDiagnoser]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
public class WritableStreamBenchmarks
{
    private byte[] _data = default!;

    [Params(1024, 65536)]
    public int Size { get; set; }

    [GlobalSetup]
    public void Setup()
    {
        _data = new byte[Size];
        new Random(42).NextBytes(_data);
    }

    // ── Construction ──

    [BenchmarkCategory("Construction"), Benchmark(Baseline = true)]
    public object Construct_MemoryStreamBase() => new WritableMemoryStreamMS(new byte[Size]);

    [BenchmarkCategory("Construction"), Benchmark]
    public object Construct_StreamBase() => new WritableMemoryStreamS(new byte[Size]);

    [BenchmarkCategory("Construction"), Benchmark]
    public object Construct_ByteArrayBaseline() => new MemoryStream(new byte[Size], writable: true);

    // ── WriteByte (most sensitive per-call benchmark) ──

    [BenchmarkCategory("WriteByte"), Benchmark(Baseline = true)]
    public void WriteByte_MemoryStreamBase()
    {
        var s = new WritableMemoryStreamMS(new byte[Size]);
        for (int i = 0; i < Size; i++) s.WriteByte((byte)i);
    }

    [BenchmarkCategory("WriteByte"), Benchmark]
    public void WriteByte_StreamBase()
    {
        var s = new WritableMemoryStreamS(new byte[Size]);
        for (int i = 0; i < Size; i++) s.WriteByte((byte)i);
    }

    [BenchmarkCategory("WriteByte"), Benchmark]
    public void WriteByte_ByteArrayBaseline()
    {
        var s = new MemoryStream(new byte[Size], writable: true);
        for (int i = 0; i < Size; i++) s.WriteByte((byte)i);
    }

    // ── WriteByte via Stream reference (polymorphic dispatch) ──

    [BenchmarkCategory("WriteByte_Polymorphic"), Benchmark(Baseline = true)]
    public void WriteBytePoly_MemoryStreamBase()
    {
        Stream s = new WritableMemoryStreamMS(new byte[Size]);
        for (int i = 0; i < Size; i++) s.WriteByte((byte)i);
    }

    [BenchmarkCategory("WriteByte_Polymorphic"), Benchmark]
    public void WriteBytePoly_StreamBase()
    {
        Stream s = new WritableMemoryStreamS(new byte[Size]);
        for (int i = 0; i < Size; i++) s.WriteByte((byte)i);
    }

    // ── WriteSpan (chunked write, 256-byte chunks) ──

    [BenchmarkCategory("WriteSpan"), Benchmark(Baseline = true)]
    public void WriteSpan_MemoryStreamBase()
    {
        var s = new WritableMemoryStreamMS(new byte[Size]);
        ReadOnlySpan<byte> chunk = _data.AsSpan(0, Math.Min(256, Size));
        for (int i = 0; i < Size / Math.Max(chunk.Length, 1); i++) s.Write(chunk);
    }

    [BenchmarkCategory("WriteSpan"), Benchmark]
    public void WriteSpan_StreamBase()
    {
        var s = new WritableMemoryStreamS(new byte[Size]);
        ReadOnlySpan<byte> chunk = _data.AsSpan(0, Math.Min(256, Size));
        for (int i = 0; i < Size / Math.Max(chunk.Length, 1); i++) s.Write(chunk);
    }

    [BenchmarkCategory("WriteSpan"), Benchmark]
    public void WriteSpan_ByteArrayBaseline()
    {
        var s = new MemoryStream(new byte[Size], writable: true);
        ReadOnlySpan<byte> chunk = _data.AsSpan(0, Math.Min(256, Size));
        for (int i = 0; i < Size / Math.Max(chunk.Length, 1); i++) s.Write(chunk);
    }

    // ── ReadByte after write (fill then read back) ──

    [BenchmarkCategory("ReadByteAfterWrite"), Benchmark(Baseline = true)]
    public int ReadByteAfterWrite_MemoryStreamBase()
    {
        var s = new WritableMemoryStreamMS(new byte[Size]);
        s.Write(_data);
        s.Position = 0;
        int count = 0;
        while (s.ReadByte() != -1) count++;
        return count;
    }

    [BenchmarkCategory("ReadByteAfterWrite"), Benchmark]
    public int ReadByteAfterWrite_StreamBase()
    {
        var s = new WritableMemoryStreamS(new byte[Size]);
        s.Write(_data);
        s.Position = 0;
        int count = 0;
        while (s.ReadByte() != -1) count++;
        return count;
    }

    [BenchmarkCategory("ReadByteAfterWrite"), Benchmark]
    public int ReadByteAfterWrite_ByteArrayBaseline()
    {
        var s = new MemoryStream(new byte[Size], writable: true);
        s.Write(_data);
        s.Position = 0;
        int count = 0;
        while (s.ReadByte() != -1) count++;
        return count;
    }

    // ── CopyTo after write ──

    [BenchmarkCategory("CopyToAfterWrite"), Benchmark(Baseline = true)]
    public void CopyToAfterWrite_MemoryStreamBase()
    {
        var s = new WritableMemoryStreamMS(new byte[Size]);
        s.Write(_data);
        s.Position = 0;
        s.CopyTo(Stream.Null);
    }

    [BenchmarkCategory("CopyToAfterWrite"), Benchmark]
    public void CopyToAfterWrite_StreamBase()
    {
        var s = new WritableMemoryStreamS(new byte[Size]);
        s.Write(_data);
        s.Position = 0;
        s.CopyTo(Stream.Null);
    }

    [BenchmarkCategory("CopyToAfterWrite"), Benchmark]
    public void CopyToAfterWrite_ByteArrayBaseline()
    {
        var s = new MemoryStream(new byte[Size], writable: true);
        s.Write(_data);
        s.Position = 0;
        s.CopyTo(Stream.Null);
    }

    // ── BinaryReader after write ──

    [BenchmarkCategory("BinaryReader"), Benchmark(Baseline = true)]
    public int BinaryReader_MemoryStreamBase()
    {
        var s = new WritableMemoryStreamMS(new byte[Size]);
        s.Write(_data);
        s.Position = 0;
        using var br = new BinaryReader(s);
        int count = 0;
        for (int i = 0; i < Size; i++) { br.ReadByte(); count++; }
        return count;
    }

    [BenchmarkCategory("BinaryReader"), Benchmark]
    public int BinaryReader_StreamBase()
    {
        var s = new WritableMemoryStreamS(new byte[Size]);
        s.Write(_data);
        s.Position = 0;
        using var br = new BinaryReader(s);
        int count = 0;
        for (int i = 0; i < Size; i++) { br.ReadByte(); count++; }
        return count;
    }

    [BenchmarkCategory("BinaryReader"), Benchmark]
    public int BinaryReader_ByteArrayBaseline()
    {
        var s = new MemoryStream(new byte[Size], writable: true);
        s.Write(_data);
        s.Position = 0;
        using var br = new BinaryReader(s);
        int count = 0;
        for (int i = 0; i < Size; i++) { br.ReadByte(); count++; }
        return count;
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// Variant A: WritableMemoryStream : MemoryStream (current API review decision)
// Source: PR #126669 — sealed subclass, all virtuals overridden, independent state
// ═══════════════════════════════════════════════════════════════════════════════

public class WritableMemoryStreamMS : MemoryStream
{
    private Memory<byte> _buffer;
    private int _position;
    private int _length;
    private bool _isOpen;

    public WritableMemoryStreamMS(Memory<byte> buffer) : base()
    {
        _buffer = buffer;
        _length = 0;
        _isOpen = true;
    }

    public override bool CanRead => _isOpen;
    public override bool CanSeek => _isOpen;
    public override bool CanWrite => _isOpen;
    public override int Capacity { get => _buffer.Length; set => throw new NotSupportedException(); }
    public override long Length { get { ThrowClosed(); return _length; } }

    public override long Position
    {
        get { ThrowClosed(); return _position; }
        set
        {
            ThrowClosed();
            ArgumentOutOfRangeException.ThrowIfNegative(value);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(value, int.MaxValue);
            _position = (int)value;
        }
    }

    public override int ReadByte()
    {
        ThrowClosed();
        ReadOnlySpan<byte> span = _buffer.Span;
        int pos = _position;
        if ((uint)pos < (uint)_length) { _position++; return span[pos]; }
        return -1;
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        ValidateBufferArguments(buffer, offset, count);
        return Read(new Span<byte>(buffer, offset, count));
    }

    public override int Read(Span<byte> buffer)
    {
        ThrowClosed();
        int remaining = _length - _position;
        if (remaining <= 0 || buffer.Length == 0) return 0;
        int n = Math.Min(remaining, buffer.Length);
        ((ReadOnlyMemory<byte>)_buffer).Span.Slice(_position, n).CopyTo(buffer);
        _position += n;
        return n;
    }

    public override void WriteByte(byte value)
    {
        ThrowClosed();
        if (_position >= _buffer.Length) throw new NotSupportedException();
        _buffer.Span[_position++] = value;
        if (_position > _length) _length = _position;
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        ValidateBufferArguments(buffer, offset, count);
        Write(new ReadOnlySpan<byte>(buffer, offset, count));
    }

    public override void Write(ReadOnlySpan<byte> buffer)
    {
        ThrowClosed();
        if (buffer.Length == 0) return;
        if (_position > _buffer.Length - buffer.Length) throw new NotSupportedException();
        buffer.CopyTo(_buffer.Span.Slice(_position));
        _position += buffer.Length;
        if (_position > _length) _length = _position;
    }

    public override void CopyTo(Stream destination, int bufferSize)
    {
        ValidateCopyToArguments(destination, bufferSize);
        ThrowClosed();
        if (_length > _position)
        {
            destination.Write(((ReadOnlyMemory<byte>)_buffer).Span.Slice(_position, _length - _position));
            _position = _length;
        }
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        ThrowClosed();
        long newPos = origin switch
        {
            SeekOrigin.Begin => offset,
            SeekOrigin.Current => _position + offset,
            SeekOrigin.End => _length + offset,
            _ => throw new ArgumentException("Invalid seek origin")
        };
        if (newPos < 0) throw new IOException("Seek before begin");
        _position = (int)newPos;
        return newPos;
    }

    public override void SetLength(long value) => throw new NotSupportedException();
    public override void Flush() { }

    public override byte[] GetBuffer() => throw new UnauthorizedAccessException();
    public override bool TryGetBuffer(out ArraySegment<byte> buffer) { buffer = default; return false; }
    public override byte[] ToArray()
    {
        ThrowClosed();
        if (_length == 0) return Array.Empty<byte>();
        byte[] copy = GC.AllocateUninitializedArray<byte>(_length);
        ((ReadOnlyMemory<byte>)_buffer).Span.Slice(0, _length).CopyTo(copy);
        return copy;
    }

    public override void WriteTo(Stream stream)
    {
        ArgumentNullException.ThrowIfNull(stream);
        ThrowClosed();
        if (_length > 0) stream.Write(((ReadOnlyMemory<byte>)_buffer).Span.Slice(0, _length));
    }

    protected override void Dispose(bool disposing) { _isOpen = false; _buffer = default; base.Dispose(disposing); }
    private void ThrowClosed() => ObjectDisposedException.ThrowIf(!_isOpen, this);
}

// ═══════════════════════════════════════════════════════════════════════════════
// Variant B: WritableMemoryStream : Stream (alternative — identical method bodies)
// ═══════════════════════════════════════════════════════════════════════════════

public class WritableMemoryStreamS : Stream
{
    private Memory<byte> _buffer;
    private int _position;
    private int _length;
    private bool _isOpen;

    public WritableMemoryStreamS(Memory<byte> buffer)
    {
        _buffer = buffer;
        _length = 0;
        _isOpen = true;
    }

    public override bool CanRead => _isOpen;
    public override bool CanSeek => _isOpen;
    public override bool CanWrite => _isOpen;
    public override long Length { get { ThrowClosed(); return _length; } }

    public override long Position
    {
        get { ThrowClosed(); return _position; }
        set
        {
            ThrowClosed();
            ArgumentOutOfRangeException.ThrowIfNegative(value);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(value, int.MaxValue);
            _position = (int)value;
        }
    }

    public override int ReadByte()
    {
        ThrowClosed();
        ReadOnlySpan<byte> span = _buffer.Span;
        int pos = _position;
        if ((uint)pos < (uint)_length) { _position++; return span[pos]; }
        return -1;
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        ValidateBufferArguments(buffer, offset, count);
        return Read(new Span<byte>(buffer, offset, count));
    }

    public override int Read(Span<byte> buffer)
    {
        ThrowClosed();
        int remaining = _length - _position;
        if (remaining <= 0 || buffer.Length == 0) return 0;
        int n = Math.Min(remaining, buffer.Length);
        ((ReadOnlyMemory<byte>)_buffer).Span.Slice(_position, n).CopyTo(buffer);
        _position += n;
        return n;
    }

    public override void WriteByte(byte value)
    {
        ThrowClosed();
        if (_position >= _buffer.Length) throw new NotSupportedException();
        _buffer.Span[_position++] = value;
        if (_position > _length) _length = _position;
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        ValidateBufferArguments(buffer, offset, count);
        Write(new ReadOnlySpan<byte>(buffer, offset, count));
    }

    public override void Write(ReadOnlySpan<byte> buffer)
    {
        ThrowClosed();
        if (buffer.Length == 0) return;
        if (_position > _buffer.Length - buffer.Length) throw new NotSupportedException();
        buffer.CopyTo(_buffer.Span.Slice(_position));
        _position += buffer.Length;
        if (_position > _length) _length = _position;
    }

    public override void CopyTo(Stream destination, int bufferSize)
    {
        ValidateCopyToArguments(destination, bufferSize);
        ThrowClosed();
        if (_length > _position)
        {
            destination.Write(((ReadOnlyMemory<byte>)_buffer).Span.Slice(_position, _length - _position));
            _position = _length;
        }
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        ThrowClosed();
        long newPos = origin switch
        {
            SeekOrigin.Begin => offset,
            SeekOrigin.Current => _position + offset,
            SeekOrigin.End => _length + offset,
            _ => throw new ArgumentException("Invalid seek origin")
        };
        if (newPos < 0) throw new IOException("Seek before begin");
        _position = (int)newPos;
        return newPos;
    }

    public override void SetLength(long value) => throw new NotSupportedException();
    public override void Flush() { }

    protected override void Dispose(bool disposing) { _isOpen = false; _buffer = default; base.Dispose(disposing); }
    private void ThrowClosed() => ObjectDisposedException.ThrowIf(!_isOpen, this);
}

Comment thread src/libraries/Common/src/System/IO/ReadOnlyMemoryStream.cs
Comment thread src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs Outdated
Comment thread src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs Outdated
Copilot AI review requested due to automatic review settings June 16, 2026 20:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 5 comments.

Copilot AI review requested due to automatic review settings June 17, 2026 06:30
@ViveliDuCh ViveliDuCh force-pushed the stream-wrappers-api branch from d8e4b7a to 8e4a441 Compare June 17, 2026 06:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 5 comments.

public class ReadOnlySequenceStreamTests
{
[Fact]
public void ReadZeroBytesReturnsZero()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a duplicate of

public virtual async Task Read_NonEmptyStream_Nop_Success(ReadWriteMode mode)
{
if (SkipOnWasi(mode)) return;
using Stream? stream = await CreateReadOnlyStream(new byte[10]);
if (stream is null)
{
return;
}
Assert.Equal(0, await ReadAsync(mode, stream, Array.Empty<byte>(), 0, 0));
Assert.Equal(0, await ReadAsync(mode, stream, new byte[0], 0, 0));
Assert.Equal(0, await ReadAsync(mode, stream, new byte[1], 0, 0));
Assert.Equal(0, await ReadAsync(mode, stream, new byte[1], 1, 0));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you try to ask copilot to cross-reference all your tests, so they are not duplicating the ones from StreamConformanceTests. That's how I found these.

}

[Fact]
public void SeekingBeyondEmptyBufferIsAllowed()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should maybe instead update Seek_PastEnd_ReadReturns0 to assert Position is updated when seeking past stream.

public virtual async Task Seek_PastEnd_ReadReturns0(SeekMode mode)
{
if (!CanSeek)
{
return;
}
const int Length = 512;
byte[] expected = GetRandomBytes(Length);
using Stream? stream = await CreateReadOnlyStream(expected);
if (stream is null)
{
return;
}
long pos = stream.Length + 10;
Assert.Equal(pos, Seek(mode, stream, pos));
Assert.Equal(0, stream.Read(new byte[1], 0, 1));
Assert.Equal(-1, stream.ReadByte());
}

Comment on lines +199 to +209
Task<int> task1 = stream.ReadAsync(buffer1, 0, 5);
Task<int> task2 = stream.ReadAsync(buffer2, 0, 5);
Task<int> task3 = stream.ReadAsync(buffer3, 0, 5);

await task1;
await task2;
await task3;

Assert.Same(task1, task2);
Assert.Same(task2, task3);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may want to reconsider this one, I don't see the Task caching optimization built into ReadOnlySequenceStream.

}

[Fact]
public async Task ReadAsyncArrayBackedMemoryUsesFastPath()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't testing any fast path I believe and there's nothing special about it. I suggest removing it.

{
return 0;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered adding a single-shot Encoding.GetBytes fast path? It is ~34% faster than using Encoder.Convert and avoids some allocations: https://gist.github.com/jozkee/ec72a16575edac7fcbde864cde61cd76.

Results:

Method Length Mean Error StdDev Ratio Gen0 Allocated Alloc Ratio
Baseline 16 40.10 ns 6.150 ns 0.337 ns 1.00 0.0100 168 B 1.00
OneShot 16 26.60 ns 2.302 ns 0.126 ns 0.66 0.0076 128 B 0.76
Baseline 1024 57.02 ns 3.955 ns 0.217 ns 1.00 0.0100 168 B 1.00
OneShot 1024 42.24 ns 8.008 ns 0.439 ns 0.74 0.0076 128 B 0.76

// cannot hold a single complete encoded character.
if (buffer.Length < _encoding.GetMaxByteCount(1))
{
_pendingBytes ??= new byte[_encoding.GetMaxByteCount(2)];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could benefit a bit from GC.AllocateUninitializedArray, as done in

_readBuffer = GC.AllocateUninitializedArray<byte>(_thisEncoding.GetMaxByteCount(_readCharBufferMaxSize));


_text = text.AsMemory();
_encoding = encoding;
_encoder = encoding.GetEncoder();

@jozkee jozkee Jun 17, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do #126669 (comment), we should lazy-init this.

_disposed = true;
base.Dispose(disposing);
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should CopyTo[Async] be overridden?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Add Stream wrappers for memory and text-based types

6 participants