Skip to content

Latest commit

 

History

History
245 lines (210 loc) · 6.79 KB

File metadata and controls

245 lines (210 loc) · 6.79 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.

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: