Table of Contents

ArrayPoolMemoryStream Class

A memory-backed stream implementation that rents fixed-size buffers from ArrayPool<byte>.Shared and stores data in multiple segments.

Namespace

CryptoHives.Foundation.Memory.Buffers

Inheritance

Object ? MarshalByRefObject ? Stream ? MemoryStream ? ArrayPoolMemoryStream

Syntax

public sealed class ArrayPoolMemoryStream : MemoryStream

Overview

ArrayPoolMemoryStream is designed for high-throughput and temporary in-memory I/O scenarios where reducing allocations and Large Object Heap (LOH) churn matters. Instead of continuously resizing a single contiguous array like the standard MemoryStream, it rents fixed-size buffers from the shared array pool and chains them together.

Benefits

  • Lower allocation churn: Rents buffers from ArrayPool<byte>.Shared instead of allocating new arrays
  • Fewer large allocations: Reuses rented buffers to avoid repeated large allocations and LOH churn
  • No resize-copy on growth: Appends new rented segments instead of reallocating and copying
  • Zero-copy multi-segment access: GetReadOnlySequence() exposes internal segments as ReadOnlySequence<byte>
  • Escapes the scope: LeaseSequence() hands the payload on with its own lifetime
  • Span APIs: Span-based read/write paths reduce intermediate allocations

Constructors

Constructor Description
ArrayPoolMemoryStream() Creates a writable stream with default buffer size (4096 bytes)
ArrayPoolMemoryStream(bool clearArray) Creates a writable stream with default sizes that zeroes its buffers on release
ArrayPoolMemoryStream(int bufferSize, bool clearArray = false) Creates a writable stream with specified buffer size
ArrayPoolMemoryStream(int bufferListSize, int bufferSize, bool clearArray = false) Creates a writable stream with custom buffer list and buffer sizes
ArrayPoolMemoryStream(int bufferListSize, int bufferSize, int start, int count, bool clearArray = false) Creates a writable stream with full customization
ArrayPoolMemoryStream(IEnumerable<ArraySegment<byte>> buffers) Creates a read-only stream from existing buffer segments

clearArray is optional on every owning constructor and defaults to false, so existing code keeps compiling and keeps its behaviour. The read-only constructor has no such parameter: it does not own the buffers it wraps and never returns them to the pool.

Sensitive Payloads

A buffer arrives from ArrayPool<byte> holding whatever the previous tenant left in it and, by default, goes back the same way. If a stream carries key material, credentials or anything else that must not outlive it, construct it with clearArray: true:

using (var stream = new ArrayPoolMemoryStream(clearArray: true))
{
    WriteKeyMaterial(stream);
    Send(stream.GetReadOnlySequence());
}   // every buffer is zeroed before it reaches the array pool

The flag is honoured on both release paths — disposal, and the buffers a SetLength truncation drops — so shrinking a stream does not quietly hand a populated array back to the pool.

This mirrors clearArray on ArrayPoolBufferWriter<T> and PooledSegment<T>.Rent. It is opt-in rather than the default because the zeroing is a real cost per buffer, and most streams carry nothing worth hiding.

Properties

Property Type Description
Length long Gets the total length of data in all segments
Position long Gets or sets the current position
CanRead bool Always returns true
CanWrite bool Returns true for writable streams, false for read-only
CanSeek bool Always returns true

Methods

Read Methods

public override int Read(byte[] buffer, int offset, int count)
public int Read(Span<byte> buffer) // .NET Standard 2.1+
public override int ReadByte()

Write Methods

public override void Write(byte[] buffer, int offset, int count)
public void Write(ReadOnlySpan<byte> buffer) // .NET Standard 2.1+
public override void WriteByte(byte value)

Seeking

public override long Seek(long offset, SeekOrigin loc)

Zero-Allocation/Zero-Copy Access

public ReadOnlySequence<byte> GetReadOnlySequence()
public bool TryCopyTo(Span<byte> destination, out int bytesWritten)

Returns a ReadOnlySequence<byte> representing all data in the stream. The sequence borrows the stream's buffers and is valid only until the next write operation or disposal. There is no allocation or copy involved. This is the recommended way to use the streamed data, but it requires extra lifetime management of the stream.

TryCopyTo provides a non-throwing copy path for callers that already own a destination buffer. The bytesWritten variable reports the copied length on success and 0 when the destination is too small.

Letting the Payload Leave the Scope

public SequenceLease<byte> LeaseSequence()

Pairs the payload with the stream itself as a single disposable value. Disposing the SequenceLease<byte> disposes the stream, which returns its buffers to the array pool.

The lease is a struct, so this costs only the segment chain GetReadOnlySequence() builds — nothing beyond what reading the payload costs anyway.

using SequenceLease<byte> payload = stream.LeaseSequence();
Send(payload.Sequence);

Length

public override void SetLength(long value)

Truncating returns the segments that fall away to the pool and keeps the rest, which makes SetLength(0) a cheap way to reuse the stream for another payload. Growing appends zeroed bytes, since a rented array carries whatever the previous tenant left in it. As with MemoryStream, the position is preserved except that one past the new end is pulled back to it.

Throws NotSupportedException on a stream over externally owned buffers, and ArgumentOutOfRangeException for a negative length or one above int.MaxValue.

Members That Cannot Expose a Single Buffer

Because the payload is spread across several pooled segments, there is no one contiguous array to hand out. These MemoryStream members are overridden accordingly:

Member Behaviour
GetBuffer() Throws NotSupportedException
TryGetBuffer(out ArraySegment<byte>) Returns false
WriteTo(Stream) Writes the whole payload, one segment at a time. Does not move the read cursor.
Capacity Gets the total capacity of the rented segments. The setter throws NotSupportedException; capacity grows automatically.

GetBuffer and TryGetBuffer refuse rather than special-casing a single-segment stream on purpose: a pooled array handed out here would be returned to the pool when the stream is disposed, trading a wrong answer for a use-after-free. Use GetReadOnlySequence() or LeaseSequence() instead.

Note

The async paths need no such care. CopyToAsync, ReadAsync and WriteAsync are guarded by MemoryStream's own derived-type check and fall back to the virtual synchronous methods, so they read and write the pooled segments correctly.

Other Methods

public override void Flush() // No-op
public override byte[] ToArray() // Copies all data to a new array
protected override void Dispose(bool disposing) // Returns buffers to pool

Usage Examples

Basic Write and Read

using CryptoHives.Foundation.Memory.Buffers;
using System;
using System.Text;

// Create a writable stream
using var stream = new ArrayPoolMemoryStream();

// Write data
byte[] data = Encoding.UTF8.GetBytes("Hello, World!");
stream.Write(data, 0, data.Length);

// Reset position
stream.Position = 0;

// Read data back
byte[] buffer = new byte[data.Length];
int bytesRead = stream.Read(buffer, 0, buffer.Length);

Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, bytesRead));
// Output: Hello, World!

Using Span APIs (.NET Standard 2.1+)

using var stream = new ArrayPoolMemoryStream();

// Write using Span
ReadOnlySpan<byte> data = stackalloc byte[] { 1, 2, 3, 4, 5 };
stream.Write(data);

// Read using Span
stream.Position = 0;
Span<byte> buffer = stackalloc byte[5];
int bytesRead = stream.Read(buffer);

Zero-Copy Access with ReadOnlySequence

using var stream = new ArrayPoolMemoryStream();

// Write data
await WriteDataAsync(stream);

// Get zero-copy sequence
ReadOnlySequence<byte> sequence = stream.GetReadOnlySequence();

// Process without copying
foreach (ReadOnlyMemory<byte> segment in sequence)
{
    ProcessSegment(segment.Span);
}

Async Write and Read

using var stream = new ArrayPoolMemoryStream();

// Async write
byte[] data = GetData();
await stream.WriteAsync(data, 0, data.Length, cancellationToken);

// Async read
stream.Position = 0;
byte[] buffer = new byte[1024];
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);

Custom Buffer Sizing

// Create stream with 8KB buffers
using var stream = new ArrayPoolMemoryStream(bufferSize: 8192);

// Create stream with custom buffer list capacity
// Zero each buffer as it returns to the pool
using var secrets = new ArrayPoolMemoryStream(bufferSize: 8192, clearArray: true);

using var stream2 = new ArrayPoolMemoryStream(
    bufferListSize: 16,  // Initial capacity for buffer list
    bufferSize: 4096     // Size of each buffer
);

Read-Only Stream from Existing Buffers

var buffers = new List<ArraySegment<byte>>
{
    new ArraySegment<byte>(buffer1),
    new ArraySegment<byte>(buffer2)
};

// Create read-only stream (buffers NOT returned to pool on dispose)
using var stream = new ArrayPoolMemoryStream(buffers);

// Read data
byte[] data = stream.ToArray();

Integration with Serialization

using var stream = new ArrayPoolMemoryStream();

// Serialize object
JsonSerializer.Serialize(stream, myObject);

// Get the data as ReadOnlySequence
ReadOnlySequence<byte> jsonBytes = stream.GetReadOnlySequence();

// Send over network without copying
await SendAsync(jsonBytes, cancellationToken);

Copying to Another Stream

using var source = new ArrayPoolMemoryStream();
await WriteDataAsync(source);

source.Position = 0;

using var destination = new FileStream("output.dat", FileMode.Create);
await source.CopyToAsync(destination);

TryCopyTo into a Preallocated Buffer

using var stream = new ArrayPoolMemoryStream();
await WriteDataAsync(stream);

Span<byte> destination = stackalloc byte[256];
if (stream.TryCopyTo(destination, out int bytesWritten))
{
    ProcessData(destination.Slice(0, bytesWritten));
}
else
{
    // destination is too small
}

Performance Considerations

When to Use

  • High-throughput transformation pipelines with many temporary streams and unknown, varying final sizes
  • Large data sets that might trigger LOH allocations
  • When you need zero-copy access to the data
  • Building tranformation pipelines with lifetime managed ReadOnlySequence<byte>

When NOT to Use

  • Very small buffers (< 1KB) - overhead might outweigh benefits
  • Long-lived streams - pooled buffers are meant for temporary use
  • When you need the data as a contiguous array anyway

Buffer Size Guidelines

  • Small data (< 4KB): Use default 4096-byte buffers
  • Medium data (4KB - 64KB): Use 8192 or 16384-byte buffers
  • Large data (> 64KB): Use 32768 or 65536-byte buffers

Thread Safety

⚠️ Not thread-safe. External synchronization required for concurrent access.

Disposal

Always dispose ArrayPoolMemoryStream to return rented buffers to the pool:

using var stream = new ArrayPoolMemoryStream();

// Use stream...
stream.Write(data);

// Process final sequence
var sequence = stream.GetReadOnlySequence();

// Automatically disposed and buffers returned

See Also


© 2026 The Keepers of the CryptoHives