Object reuse with ObjectPool in ASP.NET Core

By Steve Gordon, Ryan Nowak, and Rick Anderson

Microsoft.Extensions.ObjectPool is part of the ASP.NET Core infrastructure that supports keeping a group of objects in memory for reuse rather than allowing the objects to be garbage collected.

You might want to use the object pool if the objects that are being managed are:

  • Expensive to allocate/initialize.
  • Represent some limited resource.
  • Used predictably and frequently.

For example, the ASP.NET Core framework uses the object pool in some places to reuse StringBuilder instances. StringBuilder allocates and manages its own buffers to hold character data. ASP.NET Core regularly uses StringBuilder to implement features, and reusing them provides a performance benefit.

Object pooling doesn't always improve performance:

  • Unless the initialization cost of an object is high, it's usually slower to get the object from the pool.
  • Objects managed by the pool aren't de-allocated until the pool is de-allocated.

Use object pooling only after collecting performance data using realistic scenarios for your app or library.

WARNING: The ObjectPool doesn't implement IDisposable. We don't recommend using it with types that need disposal.

NOTE: The ObjectPool doesn't place a limit on the number of objects that it will allocate, it places a limit on the number of objects it will retain.

Concepts

ObjectPool<T> - the basic object pool abstraction. Used to get and return objects.

PooledObjectPolicy<T> - implement this to customize how an object is created and how it is reset when returned to the pool. This can be passed into an object pool that you construct directly.... OR

Create acts as a factory for creating object pools.

The ObjectPool can be used in an app in multiple ways:

  • Instantiating a pool.
  • Registering a pool in Dependency injection (DI) as an instance.
  • Registering the ObjectPoolProvider<> in DI and using it as a factory.

How to use ObjectPool

Call ObjectPool<T> to get an object and Return to return the object. There's no requirement that you return every object. If you don't return an object, it will be garbage collected.

ObjectPool sample

The following code:

  • Adds ObjectPoolProvider to the Dependency injection (DI) container.
  • Adds and configures ObjectPool<StringBuilder> to the DI container.
  • Adds the BirthdayMiddleware.
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.TryAddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();

        services.TryAddSingleton<ObjectPool<StringBuilder>>(serviceProvider =>
        {
            var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
            var policy = new StringBuilderPooledObjectPolicy();
            return provider.Create(policy);
        });
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        
        // Test using /?firstname=Steve&lastName=Gordon&day=28&month=9
        app.UseMiddleware<BirthdayMiddleware>(); 
    }
}

The following code implements BirthdayMiddleware

public class BirthdayMiddleware
{
    private readonly RequestDelegate _next;

    public BirthdayMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context, 
                                  ObjectPool<StringBuilder> builderPool)
    {
        if (context.Request.Query.TryGetValue("firstName", out var firstName) &&
            context.Request.Query.TryGetValue("lastName", out var lastName) && 
            context.Request.Query.TryGetValue("month", out var month) &&                 
            context.Request.Query.TryGetValue("day", out var day) &&
            int.TryParse(month, out var monthOfYear) &&
            int.TryParse(day, out var dayOfMonth))
        {                
            var now = DateTime.UtcNow; // Ignoring timezones.

            // Request a StringBuilder from the pool.
            var stringBuilder = builderPool.Get();

            try
            {
                stringBuilder.Append("Hi ")
                    .Append(firstName).Append(" ").Append(lastName).Append(". ");

                if (now.Day == dayOfMonth && now.Month == monthOfYear)
                {
                    stringBuilder.Append("Happy birthday!!!");

                    await context.Response.WriteAsync(stringBuilder.ToString());
                }
                else
                {
                    var thisYearsBirthday = new DateTime(now.Year, monthOfYear, 
                                                                    dayOfMonth);

                    int daysUntilBirthday = thisYearsBirthday > now 
                        ? (thisYearsBirthday - now).Days 
                        : (thisYearsBirthday.AddYears(1) - now).Days;

                    stringBuilder.Append("There are ")
                        .Append(daysUntilBirthday).Append(" days until your birthday!");

                    await context.Response.WriteAsync(stringBuilder.ToString());
                }
            }
            finally // Ensure this runs even if the main code throws.
            {
                // Return the StringBuilder to the pool.
                builderPool.Return(stringBuilder); 
            }

            return;
        }

        await _next(context);
    }
}