File: RecyclableArrayBufferWriter.cs
Web Access
Project: src\src\Middleware\OutputCaching\src\Microsoft.AspNetCore.OutputCaching.csproj (Microsoft.AspNetCore.OutputCaching)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Diagnostics;
 
namespace Microsoft.AspNetCore.OutputCaching;
 
// this is effectively a cut-down re-implementation of ArrayBufferWriter
// from https://github.com/dotnet/runtime/blob/6cd9bf1937c3b4d2f7304a6c534aacde58a202b6/src/libraries/Common/src/System/Buffers/ArrayBufferWriter.cs
// except it uses the array pool for allocations
internal sealed class RecyclableArrayBufferWriter<T> : IBufferWriter<T>, IDisposable
{
 
    // Copy of Array.MaxLength.
    // Used by projects targeting .NET Framework.
    private const int ArrayMaxLength = 0x7FFFFFC7;
 
    private const int DefaultInitialBufferSize = 256;
 
    private T[] _buffer;
    private int _index;
 
    public int FreeCapacity => _buffer.Length - _index;
 
    public RecyclableArrayBufferWriter()
    {
        _buffer = Array.Empty<T>();
        _index = 0;
    }
 
    public void Dispose()
    {
        var tmp = _buffer;
        _index = 0;
        _buffer = Array.Empty<T>();
        if (tmp.Length != 0)
        {
            ArrayPool<T>.Shared.Return(tmp);
        }
    }
 
    public void Advance(int count)
    {
        if (count < 0)
        {
            throw new ArgumentException(null, nameof(count));
        }
 
        if (_index > _buffer.Length - count)
        {
            Throw();
        }
 
        _index += count;
 
        static void Throw()
            => throw new ArgumentOutOfRangeException(nameof(count));
    }
 
    public ReadOnlyMemory<T> GetCommittedMemory() => new ReadOnlyMemory<T>(_buffer, 0, _index); // could also directly expose a ReadOnlySpan<byte> if useful
 
    public Memory<T> GetMemory(int sizeHint = 0)
    {
        CheckAndResizeBuffer(sizeHint);
        Debug.Assert(_buffer.Length > _index);
        return _buffer.AsMemory(_index);
    }
 
    public Span<T> GetSpan(int sizeHint = 0)
    {
        CheckAndResizeBuffer(sizeHint);
        Debug.Assert(_buffer.Length > _index);
        return _buffer.AsSpan(_index);
    }
 
    // create a standalone isolated copy of the buffer
    public T[] ToArray() => _buffer.AsSpan(0, _index).ToArray();
 
    private void CheckAndResizeBuffer(int sizeHint)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(sizeHint);
 
        if (sizeHint == 0)
        {
            sizeHint = 1;
        }
 
        if (sizeHint > FreeCapacity)
        {
            int currentLength = _buffer.Length;
 
            // Attempt to grow by the larger of the sizeHint and double the current size.
            int growBy = Math.Max(sizeHint, currentLength);
 
            if (currentLength == 0)
            {
                growBy = Math.Max(growBy, DefaultInitialBufferSize);
            }
 
            int newSize = currentLength + growBy;
 
            if ((uint)newSize > int.MaxValue)
            {
                // Attempt to grow to ArrayMaxLength.
                uint needed = (uint)(currentLength - FreeCapacity + sizeHint);
                Debug.Assert(needed > currentLength);
 
                if (needed > ArrayMaxLength)
                {
                    ThrowOutOfMemoryException();
                }
 
                newSize = ArrayMaxLength;
            }
 
            // resize the backing buffer
            var oldArray = _buffer;
            _buffer = ArrayPool<T>.Shared.Rent(newSize);
            oldArray.AsSpan(0, _index).CopyTo(_buffer);
            if (oldArray.Length != 0)
            {
                ArrayPool<T>.Shared.Return(oldArray);
            }
        }
 
        Debug.Assert(FreeCapacity > 0 && FreeCapacity >= sizeHint);
 
        static void ThrowOutOfMemoryException() => throw new InvalidOperationException("Unable to grow buffer as requested");
    }
}