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>.Sharedinstead 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 asReadOnlySequence<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