Table of Contents

SequenceLease<T> Struct

A payload and the disposable that ends its life, carried together as a single value — so the pair can travel through a pipeline without the consumer knowing what produced it, and without an allocation.

Namespace

CryptoHives.Foundation.Memory.Buffers

Syntax

public readonly struct SequenceLease<T> : ISequenceOwner<T>, IEquatable<SequenceLease<T>>

Type Parameters

T — The element type of the sequence.

Why a Struct

Handing a payload out of the scope that built it needs a handle carrying two things: the ReadOnlySequence<T>, and something to dispose when the buffers are finished with. As a class that handle costs an allocation — which is awkward for a package whose job is avoiding them.

As a readonly struct it costs nothing. Measured against simply holding the producer and borrowing its sequence, a lease adds 0 bytes:

pattern 1 segment 4 segments 16 segments
borrow — producer held in scope 56 B/op 224 B/op 280 B/op
lease — payload escapes the scope 56 B/op 224 B/op 280 B/op
lease boxed to ISequenceOwner<T> 104 B/op 272 B/op 328 B/op

This is the same bargain ObjectOwner<T> makes, and it comes with the same two rules: don't copy it, and don't box it.

Members

Member Type Description
Sequence ReadOnlySequence<T> The payload
Length long Total number of elements
IsEmpty bool Whether the lease holds nothing
Dispose() void Disposes the owner, releasing the payload

Constructor

public SequenceLease(ReadOnlySequence<T> sequence, IDisposable? owner);

Usually you do not call this — ArrayPoolBufferWriter<T>.LeaseSequence() and ArrayPoolMemoryStream.LeaseSequence() build one with the producer as the owner. Construct directly to lease a payload whose lifetime is managed by something else, or pass null for a payload that owns nothing.

Usage

Building inside, consuming outside

static SequenceLease<byte> BuildPayload()
{
    var writer = ObjectPools.RentBufferWriter<byte>();   // deliberately not `using`
    Serialize(writer);
    return writer.LeaseSequence();                       // the writer rides along
}

using SequenceLease<byte> payload = BuildPayload();
Send(payload.Sequence);
// dispose returns the writer to its pool, which returns the buffers

The caller never learns that a pooled ArrayPoolBufferWriter<byte> was involved. Swap the producer for an ArrayPoolMemoryStream and nothing at the call site changes.

Reading through a Stream API

using SequenceLease<byte> payload = BuildPayload();
using var reader = new ReadOnlySequenceMemoryStream(payload.Sequence);

await client.UploadAsync(reader);

An empty lease

default(SequenceLease<T>) is valid: no payload, no owner, and disposing it does nothing. Useful as a "nothing to send" return without a null check.

SequenceLease<byte> payload = condition ? BuildPayload() : default;

using (payload)
{
    if (!payload.IsEmpty) Send(payload.Sequence);
}

The Two Rules

Do not copy it

Two copies disposed means the producer is disposed twice.

using SequenceLease<byte> lease = BuildPayload();
SequenceLease<byte> copy = lease;   // don't

lease.Dispose();
copy.Dispose();                     // disposes the producer a second time

Both producers in this package tolerate a second dispose — the writer's return-to-pool is guarded so it cannot be pooled twice — so this degrades rather than corrupts. It is still a bug in the caller.

Do not box it

Casting to ISequenceOwner<T> or IDisposable allocates, which is the one thing this type exists to avoid. Measured at +48 bytes:

ISequenceOwner<byte> boxed = BuildPayload();   // allocates

That is a legitimate trade when you genuinely need polymorphism across owner kinds — just make it a choice rather than an accident.

Storing a lease in a Channel<SequenceLease<byte>>, a List<>, or an async method's locals is fine: those hold the struct inline.

Choosing Between the Two

API Cost The producer Use when
GetReadOnlySequence() 0 B you hold and dispose it yourself consumption happens inside the producer's scope
LeaseSequence() 0 B rides along; freed with the lease the payload must leave the scope

Both cost the same. The difference is only who owns the producer's lifetime: with a borrowed sequence that is you, with a lease it is the lease. So the moment a payload needs to outlive the block that built it, take the lease and do not dispose the producer yourself.

Thread Safety

Not thread-safe, and single-owner by design. The intended pattern is a short scope with using.

See Also


© 2026 The Keepers of the CryptoHives