Use ArrayPool<T> when a hot path needs temporary buffers whose size is too large or too variable for stackalloc. Pure.DI supports ArrayPool<T> out of the box, so the setup does not need to bind ArrayPool<byte>.Shared manually; request ArrayPool<byte> like any other dependency.
In this example a CSV export endpoint creates a per-request ExportBuffer. The buffer owner receives the shared ArrayPool<byte> from Pure.DI, rents an array using application options, exposes only Memory<byte> to the exporter, and returns the array when the Owned<IReportExporter> operation scope is disposed.
using Shouldly;
using Pure.DI;
using System.Buffers;
using System.Text;
DI.Setup(nameof(Composition))
.Bind().To(_ => new ExportBufferOptions(Size: 256))
.Bind().To<ExportBuffer>()
.Bind<IReportExporter>().To<CsvReportExporter>()
.Root<Owned<IReportExporter>>("Exporter");
var composition = new Composition();
var exporter = composition.Exporter;
var bytesWritten = exporter.Value.Export(
[
new Order(17, 42.50m),
new Order(18, 13.25m)
]);
bytesWritten.ShouldBeGreaterThan(0);
exporter.Value.Buffer.Returned.ShouldBeFalse();
readonly record struct Order(int Id, decimal Total);
readonly record struct ExportBufferOptions(int Size);
interface IReportExporter
{
ExportBuffer Buffer { get; }
int Export(IReadOnlyList<Order> orders);
}
sealed class CsvReportExporter(ExportBuffer buffer) : IReportExporter
{
public ExportBuffer Buffer => buffer;
public int Export(IReadOnlyList<Order> orders)
{
var writer = new ArrayBufferWriter<byte>(buffer.Memory.Length);
foreach (var order in orders)
{
var line = $"order-{order.Id},{order.Total:0.00}\n";
writer.Write(Encoding.UTF8.GetBytes(line));
}
writer.WrittenSpan.CopyTo(buffer.Memory.Span);
return writer.WrittenCount;
}
}
sealed class ExportBuffer(
ArrayPool<byte> pool,
ExportBufferOptions options)
: IDisposable
{
private byte[]? _buffer = pool.Rent(options.Size);
public Memory<byte> Memory => _buffer;
public bool Returned => _buffer is null;
public void Dispose()
{
if (_buffer is {} buffer)
{
Array.Clear(buffer);
pool.Return(buffer);
_buffer = null;
}
}
}Running this code sample locally
- Make sure you have the .NET SDK 10.0 or later installed
dotnet --list-sdk- Create a net10.0 (or later) console application
dotnet new console -n Sampledotnet add package Pure.DI
dotnet add package Shouldly- Copy the example code into the Program.cs file
You are ready to run the example 🚀
dotnet runThis pattern is useful for serialization, compression, protocol framing, and batch export pipelines. Keep the owner lifetime short, clear sensitive buffers before returning them, and do not store the rented Memory<T> after the owner is disposed.
The important DI detail is ownership: bind or auto-bind the owner as the dependency that is tracked and disposed, not the raw array. The pool itself is a built-in BCL dependency; the owner is the application-specific object that defines rent size, cleanup, and return rules. Owned<T> keeps that cleanup tied to one root call instead of the whole composition.
Warning
A rented array is never freed by the GC in a useful way: forgetting the return keeps the pool growing until every new rent allocates a fresh array. The owner must be disposed on every exit path, including exceptions, which is why Owned<T> or a using is the right boundary.
Prefer pooling only where a profiler showed buffer churn. For small fixed sizes a stackalloc-backed span is cheaper, and for a single short-lived copy a plain new byte[] is simpler. See Span and ReadOnlySpan for stack-only values and Object pool for pooled objects that are not arrays.
The following partial class will be generated
partial class Composition
{
#if NET9_0_OR_GREATER
private readonly Lock _lock = new Lock();
#else
private readonly Object _lock = new Object();
#endif
public Owned<IReportExporter> Exporter
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
var perBlockOwned = new Owned(1, _lock);
try
{
Owned<IReportExporter> perBlockOwnedIReportExporter;
// Tracks owned disposables
Owned transientOwned;
Owned localOwned1 = perBlockOwned;
transientOwned = localOwned1;
IOwned localOwned = transientOwned;
// Creates the owned value
Buffers.ArrayPool<byte> transientArrayPoolByte = Buffers.ArrayPool<byte>.Shared;
ExportBufferOptions transientExportBufferOptions = new ExportBufferOptions(Size: 256);
var transientExportBuffer = new ExportBuffer(transientArrayPoolByte, transientExportBufferOptions);
lock (_lock)
{
perBlockOwned.Add(transientExportBuffer);
}
IReportExporter localValue = new CsvReportExporter(transientExportBuffer);
perBlockOwnedIReportExporter = new Owned<IReportExporter>(localValue, localOwned);
lock (_lock)
{
perBlockOwned.Add(perBlockOwnedIReportExporter);
}
return perBlockOwnedIReportExporter;
}
catch
{
if (!Object.ReferenceEquals(perBlockOwned, null))
{
try
{
((IDisposable)perBlockOwned).Dispose();
}
catch
{
// Preserve the original graph construction exception.
}
}
throw;
}
}
}
}Class diagram:
---
config:
class:
hideEmptyMembersBox: true
---
classDiagram
Owned --|> IOwned
ExportBufferOptions --|> IEquatableᐸExportBufferOptionsᐳ
CsvReportExporter --|> IReportExporter
Composition ..> OwnedᐸIReportExporterᐳ : OwnedᐸIReportExporterᐳ Exporter
ExportBuffer *-- ExportBufferOptions : ExportBufferOptions
ExportBuffer *-- ArrayPoolᐸByteᐳ : ArrayPoolᐸByteᐳ
CsvReportExporter *-- ExportBuffer : ExportBuffer
OwnedᐸIReportExporterᐳ *-- Owned : IOwned
OwnedᐸIReportExporterᐳ *-- CsvReportExporter : IReportExporter
namespace Pure.DI {
class IOwned {
<<interface>>
}
class Owned {
<<class>>
}
class OwnedᐸIReportExporterᐳ {
<<struct>>
}
}
namespace Pure.DI.UsageTests.HighPerformance.ArrayPoolBufferScenario {
class Composition {
<<partial>>
+OwnedᐸIReportExporterᐳ Exporter
}
class CsvReportExporter {
<<class>>
+CsvReportExporter(ExportBuffer buffer)
}
class ExportBuffer {
<<class>>
+ExportBuffer(ArrayPoolᐸByteᐳ pool, ExportBufferOptions options)
}
class ExportBufferOptions {
<<record>>
}
class IReportExporter {
<<interface>>
}
}
namespace System {
class IEquatableᐸExportBufferOptionsᐳ {
<<interface>>
}
}
namespace System.Buffers {
class ArrayPoolᐸByteᐳ {
<<abstract>>
}
}
See also: