Skip to content

[API Proposal]: Amend WritableMemoryStream and ReadOnlyMemoryStream to derive from Stream, not MemoryStream #130330

Description

@jozkee

Background and motivation

.NET 11 adds two new public System.IO types, both currently deriving from MemoryStream:

public sealed class WritableMemoryStream : MemoryStream;
public sealed class ReadOnlyMemoryStream : MemoryStream;

Each wraps a Memory<byte>/ReadOnlyMemory<byte>, respectively. However, I think we should amend #82801 to use Stream as the base class given that none of the members MemoryStream adds on top of Stream are useful on these types, they are either deliberately disabled (GetBuffer/TryGetBuffer) or redundant, as detailed below.

  • byte[] GetBuffer() and bool TryGetBuffer(out ArraySegment<byte>) are both deliberately overridden to be inert (GetBuffer always throws UnauthorizedAccessException, TryGetBuffer always returns false). They are disabled because these types are always over a user-supplied buffer and never opt into MemoryStream's publiclyVisible exposure: exposure can't work for ReadOnlyMemoryStream (its accessors return writable byte[]/ArraySegment<byte> over read-only memory); it could work for WritableMemoryStream but only when the backing of Memory<byte> is an array, so it is intentionally left off to keep the two aligned.
  • int Capacity returns _memory.Length, which equals Length for these fixed buffers, making it redundant.
  • byte[] ToArray() is just a caller-side copy that any Stream consumer can perform with Read/CopyTo, so it adds nothing unique.
  • void WriteTo(Stream) is identical in effect to Stream.CopyTo(Stream), so it is redundant as well.

The change also removes a footgun for code that type-checks is MemoryStream and then calls GetBuffer/TryGetBuffer (which throw / return false here) turning a runtime surprise into a compile-time absence.

As a bonus, changing the base to Stream shrinks each instance from 80 B to 56 B (‑30%). The savings comes from the unused MemoryStream fields: _buffer, _capacity, _expandable, _exposable (never read) plus _origin (always 0), _writable (constant per type), and _length (equals _memory.Length). Each type then needs only _memory, _position, _isOpen, and _lastReadTask.

cc @ViveliDuCh @adamsitnik

API Proposal

namespace System.IO;

-public sealed partial class WritableMemoryStream : MemoryStream
+public sealed partial class WritableMemoryStream : Stream
{
-   public override int Capacity { get; set; }
-   public override byte[] GetBuffer();
-   public override bool TryGetBuffer(out ArraySegment<byte> buffer);
-   public override byte[] ToArray();
-   public override void WriteTo(Stream stream);
}

-public sealed partial class ReadOnlyMemoryStream : MemoryStream
+public sealed partial class ReadOnlyMemoryStream : Stream
{
-   public override int Capacity { get; set; }
-   public override byte[] GetBuffer();
-   public override bool TryGetBuffer(out ArraySegment<byte> buffer);
-   public override byte[] ToArray();
-   public override void WriteTo(Stream stream);
}

API Usage

N/A, usage is identical; only the base class and the redundant MemoryStream-specific members change. Code consuming these as Streams is unaffected.

Alternative (additional) Designs

Expose the underlying buffer without having to opt-into it.

public sealed partial class WritableMemoryStream : Stream
{
+    public Memory<byte> GetMemory();
}

public sealed partial class ReadOnlyMemoryStream : Stream
{
+    public ReadOnlyMemory<byte> GetReadOnlyMemory();
}

No response

Risks

Preview-only break: code assigning to MemoryStream or matching is MemoryStream breaks.

Additional Information

But the types have MemoryStream in their name...

There's precedent of that being overthrown by UnmanagedMemoryStream.

MemoryStream has so many modes.

Extending on the dangers of deriving from MemoryStream, there's cases where code is unaware of other modalities and don't handle it correctly or discourage them:

These cases are already broken for when MemoryStream is used with a user-buffer but thought it would be worth mentioning them since these types are in user-buffer mode.

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarea-System.IO

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions