Skip to content

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

Open
ViveliDuCh wants to merge 4 commits intodotnet:mainfrom
ViveliDuCh:stream-wrappers-api
Open

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

Conversation

@ViveliDuCh
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
  • TryGetBuffer returns false and GetBuffer throws on the memory stream types (no publiclyVisible support for now)
  • ReadOnlyMemoryStream and WritableMemoryStream derive from Stream directly. Deriving from MemoryStream is the approved shape but deferred pending perf assessment of the private-fields constraint

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

  • Deriving ReadOnlyMemoryStream / WritableMemoryStream from MemoryStream (follow-up after base class perf assessment)
  • IBufferWriter<byte>Stream wrapper (separate proposal per #100434)

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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);

if (absolutePosition < 0)
{
throw new IOException(SR.IO_SeekBeforeBegin);
}
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.

Position = -1 will throw an ArgumentOutOfRangeException but Seek(-1, SeekOrigin.Begin) will throw an IOException. Is that consistent with what we do in other Stream types?

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.

As per MemoryStream and UnmanagedMemoryStream, it is: In both, Position setter throws ArgumentOutOfRangeException for negatives, while Seek throws IOException.

/// <summary>
/// Gets the encoding used by this stream.
/// </summary>
public Encoding Encoding => _encoding;
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'm curious why we decided to expose the Encoding instance. What's the use case for accessing it?

public override int Read(System.Span<byte> buffer) { throw null; }
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) { }
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 ref is only including overrides for the abstract members whereas the other refs later are also including overrides for the virtual members?

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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 on lines +58 to +66
/// <inheritdoc />
public override long Position
{
get
{
EnsureNotDisposed();
return _positionPastEnd >= 0 ? _positionPastEnd : _sequence.Slice(_sequence.Start, _position).Length;
}
set
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

ReadOnlySequenceStream.Position computes the current offset via _sequence.Slice(_sequence.Start, _position).Length each time. On multi-segment sequences this is O(segments) per call and can become very expensive (e.g., SeekOrigin.Current / Position-heavy loops). Consider tracking a numeric position field updated on Read/Seek/Position set, using SequencePosition only as an iterator cursor.

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +45
// Both MemoryStream (fixed capacity) and WritableMemoryStream throw NotSupportedException
// when trying to expand beyond capacity, just with different messages
var exception = Assert.Throws<NotSupportedException>(() =>
stream.Write(data, 0, data.Length));

// Accept either message format: WritableMemoryStream's or MemoryStream's 'SR.NotSupported_MemStreamNotExpandable' message
Assert.True(
exception.Message.Contains("Cannot expand buffer") ||
exception.Message.Contains("not expandable"),
$"Unexpected exception message: {exception.Message}");
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

These tests assert on English substrings in NotSupportedException.Message ("Cannot expand buffer" / "not expandable"), which is culture- and implementation-dependent and can fail under localization or message updates. Prefer asserting only on the exception type (and, if needed, observable behavior like Position/Length staying unchanged).

Copilot uses AI. Check for mistakes.
Comment on lines +57 to +65
// Both MemoryStream (fixed capacity) and WritableMemoryStream throw NotSupportedException
var exception = Assert.Throws<NotSupportedException>(() => stream.WriteByte(4));

// Accept either message format: WritableMemoryStream's or MemoryStream's 'SR.NotSupported_MemStreamNotExpandable' message
Assert.True(
exception.Message.Contains("Cannot expand buffer") ||
exception.Message.Contains("not expandable"),
$"Unexpected exception message: {exception.Message}");
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

Same issue here: asserting on exception message substrings makes the test brittle across localization and message changes. Prefer asserting on exception type and/or state invariants rather than Message contents.

Copilot uses AI. Check for mistakes.
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

The file now starts with a UTF-8 BOM (invisible char before <Project ...>). Most repo .csproj files are BOM-less; this encoding change creates noisy diffs and can trip tooling. Please remove the BOM so the first character is '<'.

Suggested change
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

Copilot uses AI. Check for mistakes.
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

This .csproj now has a UTF-8 BOM at the start of the file. Many other csproj files in the repo (e.g., src/libraries/System.Runtime/src/System.Runtime.csproj) are BOM-less; please remove the BOM to keep encoding consistent and avoid noisy diffs.

Suggested change
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

Copilot uses AI. Check for mistakes.
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

This .csproj now has a UTF-8 BOM at the start of the file. Please remove the BOM to keep encoding consistent with other csproj files in the repo and reduce diff noise.

Suggested change
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +18
/// <para>The stream cannot be written to. <see cref="CanWrite"/> always returns <see langword="false"/>.</para>
/// <para><see cref="GetBuffer"/> throws and <see cref="TryGetBuffer"/> returns <see langword="false"/>.</para>
/// </remarks>
public sealed class ReadOnlyMemoryStream : MemoryStream
{
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

PR description notes ReadOnlyMemoryStream/WritableMemoryStream derive directly from Stream and that deriving from MemoryStream was deferred, but the implementation (and ref) currently inherits MemoryStream. Please align the PR description with the shipped API shape, or adjust the implementation if the intent is still Stream-based.

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +25
[Fact]
public void Constructor_DefaultParameters_CreatesPubliclyVisibleStream()
{
byte[] buffer = new byte[100];
Stream stream = new ReadOnlyMemoryStream(new ReadOnlyMemory<byte>(buffer));

Assert.True(stream.CanRead);
Assert.False(stream.CanWrite);
Assert.True(stream.CanSeek);
Assert.Equal(100, stream.Length);
Assert.Equal(0, stream.Position);
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

Test name suggests the stream is "publicly visible", but ReadOnlyMemoryStream explicitly disallows GetBuffer/TryGetBuffer per its remarks/API. Consider renaming this test to reflect what it's actually validating (e.g., basic capabilities/length/position) to avoid confusion.

Copilot uses AI. Check for mistakes.
// Write only stream - no write support
protected override Task<Stream?> CreateWriteOnlyStreamCore(byte[]? initialData) => Task.FromResult<Stream?>(null);

// Read only stream - no read/write support
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

Comment says "Read only stream - no read/write support" on CreateReadWriteStreamCore, but this override is specifically for a read-write stream factory returning null (because the type doesn't support read-write). Consider updating the comment to avoid implying a read-only stream is being created here.

Suggested change
// Read only stream - no read/write support
// Read/write stream - no read/write support

Copilot uses AI. Check for mistakes.
@ViveliDuCh ViveliDuCh force-pushed the stream-wrappers-api branch from 95a2989 to 5e3f98c Compare April 9, 2026 19:59
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

3 participants