TL;DR: .NET 11 adds StringStream, ReadOnlyMemoryStream, and WritableMemoryStream. They wrap a string, a ReadOnlyMemory<byte>, or a Memory<byte> as a Stream, so you stop copying data into a MemoryStream just to satisfy a method signature.

This ships in .NET 11, which is at Release Candidate 2 with a go-live license, but is not GA yet. Everything below was run on the .NET 11 RC 1 SDK.

The line everybody has written

Some API wants a Stream. You have a string. So you write the line that has been in every codebase since 2002:

using var stream = new MemoryStream(Encoding.UTF8.GetBytes(text)); 

It works. It also encodes the entire string into a brand new byte[] before the parser reads a single byte.

The same story repeats with buffers. You already have a ReadOnlyMemory<byte> from a pool, a socket, or a slice, and the only way to hand it to a Stream API was to call ToArray() and copy it, or write your own little wrapper class. Most teams have that wrapper somewhere, usually named MemoryStreamButBetter.

What .NET 11 adds

Four new Stream types that sit directly on top of memory you already have:

  • StringStream reads a string or ReadOnlyMemory<char> as bytes in the encoding you pick.
  • ReadOnlyMemoryStream exposes a ReadOnlyMemory<byte> as a read-only, seekable stream.
  • WritableMemoryStream exposes a Memory<byte> as a fixed-size, writable stream.
  • ReadOnlySequenceStream exposes a ReadOnlySequence<byte>, for example from System.IO.Pipelines, without flattening it.

The first three live in System.IO. ReadOnlySequenceStream lives in System.Buffers, so look there before you decide it is missing. I checked on the .NET 10 SDK: none of the four types exist there.

The minimal version

using System.Text; using System.Text.Json; string json = """{"name":"Ada","langs":["C#","F#"]}"""; using var stream = new StringStream(json, Encoding.UTF8); var doc = JsonDocument.Parse(stream); Console.WriteLine(doc.RootElement.GetProperty("name")); // Ada 

No GetBytes, no intermediate array.

To see whether that matters, I fed a 1,000,000 character string through both versions and read each stream to the end with a 4 KB buffer, measuring with GC.GetAllocatedBytesForCurrentThread() on a warm second run:

Approach, 1,000,000 ASCII chars Allocated
new MemoryStream(Encoding.UTF8.GetBytes(text)) 1,000,088 bytes
new StringStream(text, Encoding.UTF8) 288 bytes

The old way allocated a copy the size of the whole string. The new way allocated a few hundred bytes. Your garbage collector will not send a thank-you card, but it will be thinking it.

The realistic one: serialize into a pooled buffer

Here is where the byte-side types shine. Rent a buffer, serialize into it, then read it back, with no MemoryStream and no ToArray():

using System.Buffers; using System.Text; using System.Text.Json; byte[] rented = ArrayPool<byte>.Shared.Rent(256); try { int written; using (var w = new WritableMemoryStream(rented)) { JsonSerializer.Serialize(w, new { Id = 42, Sku = "A1", Qty = 3 }); written = (int)w.Position; } ReadOnlyMemory<byte> json = rented.AsMemory(0, written); Console.WriteLine(Encoding.UTF8.GetString(json.Span)); using var r = new ReadOnlyMemoryStream(json); var back = JsonSerializer.Deserialize<JsonElement>(r); Console.WriteLine($"round trip qty = {back.GetProperty("Qty")}"); } finally { ArrayPool<byte>.Shared.Return(rented); } 

The output:

{"Id":42,"Sku":"A1","Qty":3} round trip qty = 3 

The pool handed back 256 bytes, the serializer wrote 28 of them, and the reader saw exactly those 28. The same trick works for an HTTP body: new StreamContent(new ReadOnlyMemoryStream(payload)) sends a buffer you already own without copying it first.

StringStream is just as handy in tests. XDocument.Load(new StringStream(xml, Encoding.UTF8)) parsed an inline XML literal in my run, which beats keeping a fixture file for a one-line order.

Can I use it?

  • .NET 11 only. The types are in the shared framework, so there is no package to add.
  • Status: .NET 11 is at RC 2 with a go-live license; GA is still ahead.
  • Docs: each type has an API reference page under the net-11.0 view on Microsoft Learn.

Gotchas

  1. StringStream cannot seek. In my run it reported CanSeek = False, and reading Length threw NotSupportedException. It does not know the byte count without encoding the whole string, which is the work it exists to avoid. If an API insists on Length or rewinding, this is the wrong type for it.

  2. WritableMemoryStream never grows. Write past the end of the buffer and you get NotSupportedException: Unable to expand length of this stream beyond its capacity. Serializing a small object into an 8-byte buffer failed exactly that way. Size the buffer for the worst case, or keep MemoryStream for output of unknown size.

  3. Length is the buffer, not what you wrote. A WritableMemoryStream over an 8-byte buffer reported Length = 8 before a single write, and still 8 after writing 3 bytes. Use Position to find how many bytes you actually wrote, as the pooled example does.

  4. StringStream does not write a BOM. Even new UTF8Encoding(true) produced just 68 69 for "hi" in my test, with no EF BB BF in front. If a consumer needs the preamble, write it yourself first.

  5. ReadOnlySequenceStream is not seekable in RC 1. Reading its Length threw "Stream does not support seeking." on the RC 1 SDK. Treat it as a forward-only reader over pipeline buffers.

Wrap-up

Four small types, one less copy every time an API demands a Stream. Grab the .NET 11 RC SDK, search your code for new MemoryStream(Encoding.UTF8.GetBytes(, and see how many you can delete.

Sources

This article was written with the help of AI and checked against the sources above.