Add Stream wrappers for memory and text-based types#126669
Add Stream wrappers for memory and text-based types#126669ViveliDuCh wants to merge 4 commits intodotnet:mainfrom
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
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) andReadOnlySequenceStream(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);
...m.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs
Outdated
Show resolved
Hide resolved
...m.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs
Outdated
Show resolved
Hide resolved
...aries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs
Show resolved
Hide resolved
...aries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs
Show resolved
Hide resolved
...aries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/Resources/Strings.resx
Outdated
Show resolved
Hide resolved
src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs
Show resolved
Hide resolved
| if (absolutePosition < 0) | ||
| { | ||
| throw new IOException(SR.IO_SeekBeforeBegin); | ||
| } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
As per MemoryStream and UnmanagedMemoryStream, it is: In both, Position setter throws ArgumentOutOfRangeException for negatives, while Seek throws IOException.
src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs
Show resolved
Hide resolved
| /// <summary> | ||
| /// Gets the encoding used by this stream. | ||
| /// </summary> | ||
| public Encoding Encoding => _encoding; |
There was a problem hiding this comment.
I'm curious why we decided to expose the Encoding instance. What's the use case for accessing it?
src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs
Outdated
Show resolved
Hide resolved
| 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) { } |
There was a problem hiding this comment.
This ref is only including overrides for the abstract members whereas the other refs later are also including overrides for the virtual members?
27761ff to
b43698b
Compare
b43698b to
95a2989
Compare
| /// <inheritdoc /> | ||
| public override long Position | ||
| { | ||
| get | ||
| { | ||
| EnsureNotDisposed(); | ||
| return _positionPastEnd >= 0 ? _positionPastEnd : _sequence.Slice(_sequence.Start, _position).Length; | ||
| } | ||
| set |
There was a problem hiding this comment.
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.
| // 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}"); | ||
| } |
There was a problem hiding this comment.
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).
| // 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}"); | ||
| } |
There was a problem hiding this comment.
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.
| @@ -1,4 +1,4 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk"> | |||
| <Project Sdk="Microsoft.NET.Sdk"> | |||
There was a problem hiding this comment.
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 '<'.
| <Project Sdk="Microsoft.NET.Sdk"> | |
| <Project Sdk="Microsoft.NET.Sdk"> |
| @@ -1,4 +1,4 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk"> | |||
| <Project Sdk="Microsoft.NET.Sdk"> | |||
There was a problem hiding this comment.
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.
| <Project Sdk="Microsoft.NET.Sdk"> | |
| <Project Sdk="Microsoft.NET.Sdk"> |
| @@ -1,4 +1,4 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk"> | |||
| <Project Sdk="Microsoft.NET.Sdk"> | |||
There was a problem hiding this comment.
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.
| <Project Sdk="Microsoft.NET.Sdk"> | |
| <Project Sdk="Microsoft.NET.Sdk"> |
| /// <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 | ||
| { |
There was a problem hiding this comment.
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.
| [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); | ||
| } |
There was a problem hiding this comment.
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.
| // Write only stream - no write support | ||
| protected override Task<Stream?> CreateWriteOnlyStreamCore(byte[]? initialData) => Task.FromResult<Stream?>(null); | ||
|
|
||
| // Read only stream - no read/write support |
There was a problem hiding this comment.
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.
| // Read only stream - no read/write support | |
| // Read/write stream - no read/write support |
…onvert edge case. Flush strategy update.
95a2989 to
5e3f98c
Compare
Fixes #82801
Introduces standardized stream wrappers for text and memory types as public sealed classes (
StringStream,ReadOnlyMemoryStream,WritableMemoryStream, andReadOnlySequenceStream) as approved in API review (video).What's included
StringStream(System.IO, CoreLib): non-seekable, read-only stream that encodesstringorReadOnlyMemory<char>on-the-fly. Encoding is required (no default). Never emits BOMs.ReadOnlyMemoryStream(System.IO, CoreLib): seekable, read-only stream overReadOnlyMemory<byte>WritableMemoryStream(System.IO, CoreLib): seekable, read-write stream overMemory<byte>with fixed capacityReadOnlySequenceStream(System.Buffers, System.Memory): seekable, read-only stream overReadOnlySequence<byte>System.RuntimeandSystem.MemoryReadOnlyMemoryContent,XmlPreloadedResolver,JsonXmlDataContract) to use the new typesImplementation notes
public sealedwith direct constructors: the review moved away from factory methods onStreamto avoid derived-type IntelliSense noiseStringStreamencodes directly into the caller's buffer inRead()rather than using an internal byte bufferTryGetBufferreturns false andGetBufferthrows on the memory stream types (nopubliclyVisiblesupport for now)ReadOnlyMemoryStreamandWritableMemoryStreamderive fromStreamdirectly. Deriving fromMemoryStreamis the approved shape but deferred pending perf assessment of the private-fields constraintLimitations
ReadOnlyMemoryStreaminCommon/cannot be fully replaced:System.Memory.Datamulti-targetsnetstandard2.0where the new public type doesn't exist.System.Net.Http(net11.0 only) can adopt it directly.Follow-up work
ReadOnlyMemoryStream/WritableMemoryStreamfromMemoryStream(follow-up after base class perf assessment)IBufferWriter<byte>→Streamwrapper (separate proposal per #100434)