using System.Collections.Generic;
using System.Collections;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
public sealed class RentedArrayWrapper<T> : IList<T>, IDisposable
readonly ArrayPool<T>? pool;
public static RentedArrayWrapper<T> Create(ArrayPool<T> pool, int count) => new RentedArrayWrapper<T>(pool.Rent(count), pool, count);
RentedArrayWrapper(T [] array, ArrayPool<T>? pool,int count)
if (count < 0 || count > array.Length)
throw new ArgumentException("count < 0 || count > array.Length");
this.array = array ?? throw new ArgumentNullException(nameof(array));
public T [] Array => array ?? throw new ObjectDisposedException(GetType().Name);
public Memory<T> Memory => Array.AsMemory().Slice(0, count);
if (index < 0 || index >= count)
throw new ArgumentOutOfRangeException();
if (index < 0 || index >= count)
throw new ArgumentOutOfRangeException();
public IEnumerable<T> EnumerateAndDispose()
IEnumerable<T> EnumerateAndDisposeInner()
foreach (var item in this)
return EnumerateAndDisposeInner();
public IEnumerator<T> GetEnumerator()
IEnumerator<T> GetEnumeratorInner()
for (int i = 0; i < count; i++)
return GetEnumeratorInner();
public int IndexOf(T item) => System.Array.IndexOf<T>(Array, item, 0, count);
public bool Contains(T item) => IndexOf(item) >= 0;
public void CopyTo(T[] array, int arrayIndex) => Memory.CopyTo(array.AsMemory().Slice(arrayIndex));
public int Count => count;
void IList<T>.Insert(int index, T item) => throw new NotImplementedException();
void IList<T>.RemoveAt(int index) => throw new NotImplementedException();
void ICollection<T>.Add(T item) => throw new NotImplementedException();
void ICollection<T>.Clear() => throw new NotImplementedException();
bool ICollection<T>.Remove(T item) => throw new NotImplementedException();
bool ICollection<T>.IsReadOnly => true;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
GC.SuppressFinalize(this);
throw new ObjectDisposedException(GetType().Name);
void Dispose(bool disposing)
if (Interlocked.Exchange(ref this.array, null!) is {} array)
public static partial class ArrayPoolExtensions
public static RentedArrayWrapper<T> RentWrapper<T>(this ArrayPool<T> pool, int count) => RentedArrayWrapper<T>.Create(pool, count);
public class OkDisposableResult : OkObjectResult
public OkDisposableResult(IDisposable disposable) : base(disposable) { }
public override async Task ExecuteResultAsync(ActionContext context)
await base.ExecuteResultAsync(context);
if (Value is IDisposable disposable)
public override void ExecuteResult(ActionContext context)
base.ExecuteResult(context);
if (Value is IDisposable disposable)
public class DummyController : ControllerBase
readonly ArrayPool<int> _defaultArrayPool = ArrayPool<int>.Shared;
public IActionResult ParentFunction()
var wrapper = InnerFunction(1000);
return new OkDisposableResult(wrapper);
public RentedArrayWrapper<int> InnerFunction(int count)
var wrapper = _defaultArrayPool.RentWrapper(count);
public static class HttpOkResultTest
public static async Task<Stream> Test(OkObjectResult result)
var httpContext = new DefaultHttpContext
RequestServices = CreateServices(),
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
await result.ExecuteResultAsync(actionContext);
Assert.That(StatusCodes.Status200OK == httpContext.Response.StatusCode);
var body = actionContext.HttpContext.Response.Body;
private static IServiceProvider CreateServices()
var options = Options.Create(new MvcOptions());
options.Value.OutputFormatters.Add(new StringOutputFormatter());
options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new JsonOptions().JsonSerializerOptions));
var services = new ServiceCollection();
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
new DefaultOutputFormatterSelector(options, NullLoggerFactory.Instance),
new TestHttpResponseStreamWriterFactory(),
NullLoggerFactory.Instance,
return services.BuildServiceProvider();
public class TestHttpResponseStreamWriterFactory : Microsoft.AspNetCore.Mvc.Infrastructure.IHttpResponseStreamWriterFactory
public const int DefaultBufferSize = 16 * 1024;
public TextWriter CreateWriter(Stream stream, Encoding encoding)
return new Microsoft.AspNetCore.WebUtilities.HttpResponseStreamWriter(stream, encoding, DefaultBufferSize);
public static async Task Main()
var array = ArrayPool<int>.Shared.Rent(100);
for (int i = 0; i < 10; i++)
var jsonBeforeReturn = JsonSerializer.Serialize(array.Take(10));
ArrayPool<int>.Shared.Return(array);
var array2 = ArrayPool<int>.Shared.Rent(100);
for (int i = 0; i < 10; i++)
ArrayPool<int>.Shared.Return(array2);
await TestRentedArrayWrapper();
var returnedJson = JsonSerializer.Serialize(array.Take(10));
Console.WriteLine("\n\nResult of serializing a rented array before and after being returned:\n {0}\n {1}", jsonBeforeReturn, returnedJson);
static async Task TestRentedArrayWrapper()
var controller = new DummyController();
var result = controller.ParentFunction();
var stream = await HttpOkResultTest.Test((OkDisposableResult)result);
var text = new StreamReader(stream).ReadToEnd();
Assert.ThrowsAsync<ObjectDisposedException>(() => HttpOkResultTest.Test((OkDisposableResult)result));