Skip to content

Latest commit

 

History

History
249 lines (213 loc) · 7.4 KB

arraypool-buffer.md

File metadata and controls

249 lines (213 loc) · 7.4 KB

ArrayPool buffer

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
dotnet --list-sdk
  • Create a net10.0 (or later) console application
dotnet new console -n Sample
dotnet 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 run

This 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>>
		}
	}
Loading

See also: