Output caching in .NET 8 API

Chaitanya (Chey) Penmetsa
CodeNx
Published in
6 min readMay 4, 2024

This article focuses on the implementation of Output Caching in ASP.NET. In ASP.NET Core 7, Output Caching has become a built-in feature, eliminating the need for manual implementation. Output Caching is a strategy utilized in ASP.NET Core to cache frequently accessed data, primarily aimed at enhancing performance. By mitigating repetitive requests to resource-intensive dependencies, such as databases or network calls, we can significantly enhance our application’s response times. This optimization is crucial for effectively scaling applications. Previously, developers had to handle these functionalities themselves. This integration by Microsoft is a significant advantage. We have seen different types of caching in our previous blog, for in-depth understanding on differences between please read previous blog using below link:

Now we will setup a simple web api using .NET 8 and demonstrate Output cache setup, caching policies, and cache keys. Depending on the length of the blog we will learn all these concepts over single blog or two blogs.

Setup API and add Output cache

First let us setup an API for creating and querying customers, we will be storing the customers in MongoDb.

[Route("api/[controller]")]
[ApiController]
public class CustomerController : ControllerBase
{
private readonly ICustomerRepository _customerRepository;

public CustomerController(ICustomerRepository customerRepository)
{
_customerRepository = customerRepository;
}

[HttpGet]
public async Task<IActionResult> GetCustomersAsync([FromQuery] string? city=null)
{
var customers = await _customerRepository.GetCustomersAsync(city);
var customerResponses = new List<GetCustomerResponse>();
foreach (var customer in customers)
{
var customerResponse = new GetCustomerResponse()
{
Id = customer.Id,
LastName = customer.LastName,
FirstName = customer.FirstName,
City = customer.City,
Email = customer.Email,
};
customerResponses.Add(customerResponse);
}

return Ok(customerResponses);
}

[HttpPost]
public async Task<IActionResult> CreateCustomerAsync([FromBody] CreateCustomerRequest createCustomerRequest)
{
var totalCustomerCount = await _customerRepository.GetTotalDocumentsCountAsync();
var customer = new Customer()
{
Id = (totalCustomerCount + 1).ToString(),
FirstName = createCustomerRequest.FirstName,
LastName = createCustomerRequest.LastName,
Email = createCustomerRequest.Email,
City = createCustomerRequest.City
};
await _customerRepository.CreateCustomerAsync(customer);
return Ok(customer.Id);
}
}

In the example provided, each time the GetCustomers method is invoked, a request is sent to MongoDB to retrieve customer data. However, if we assume that customer data doesn’t change frequently, it would be inefficient to constantly query the database. In such cases, it’s more resource-efficient to cache the output and minimize resource consumption. Now let us see how we setup Output Caching for this API by adding the middleware as shown below to service collection and request pipeline.

using BusinessLogic.Data;

var builder = WebApplication.CreateBuilder(args);

// This will load the MongoDb settings
builder.Services.Configure<MongoDbSettings>(builder.Configuration.GetSection(nameof(MongoDbSettings)));

// Add services to the container.
builder.Services.AddScoped<ICustomerRepository, CustomerRepository>();

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

//Caching
builder.Services.AddOutputCache();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllers();
app.UseOutputCache();

app.Run();

Now add [OutputCache] for GetCustomers as shown below.

[HttpGet]
[OutputCache]
public async Task<IActionResult> GetCustomersAsync([FromQuery] string? city=null)
{
var customers = await _customerRepository.GetCustomersAsync(city);
var customerResponses = new List<GetCustomerResponse>();
foreach (var customer in customers)
{
var customerResponse = new GetCustomerResponse()
{
Id = customer.Id,
LastName = customer.LastName,
FirstName = customer.FirstName,
City = customer.City,
Email = customer.Email,
};
customerResponses.Add(customerResponse);
}

return Ok(customerResponses);
}

When you initiate the request from Swagger, first time you will see the call being sent to MongoDb. But when you make same call from swagger it will return cached response. By default, the response will be cached up to a minute. It seems very simple, but this will reduce the resource consumption and improves application performance. Let us say we got a requirement that GetCustomers can be only cached for 10 seconds instead of a minute, we can achieve that using Caching Policies.

Caching Policies

Caching policies define the rules and behaviors governing how caching is managed. For above requirement, we can use Expiration Policy as shown below while configuring the Output caching or at individual endpoint.

[HttpGet]
[OutputCache(Duration =10)] //Cache for 10 seconds
public async Task<IActionResult> GetCustomersAsync([FromQuery] string? city=null)
{
var customers = await _customerRepository.GetCustomersAsync(city);
var customerResponses = new List<GetCustomerResponse>();
foreach (var customer in customers)
{
var customerResponse = new GetCustomerResponse()
{
Id = customer.Id,
LastName = customer.LastName,
FirstName = customer.FirstName,
City = customer.City,
Email = customer.Email,
};
customerResponses.Add(customerResponse);
}

return Ok(customerResponses);
}

Also, you can define Caching policies as shown below and use them at different points.

//Caching
builder.Services.AddOutputCache(options =>
{
// By default all the outputs will be cached for 30 seconds
options.AddBasePolicy(builder =>
builder.Expire(TimeSpan.FromSeconds(30)));

//Which ever endpoint using this policy that paritcular endpoint will be cached for 10 seconds
options.AddPolicy("CacheForTenSeconds", builder =>
builder.Expire(TimeSpan.FromSeconds(10)));
});
 [HttpGet]
//[OutputCache(Duration =10)] //Cache for 10 seconds
[OutputCache(PolicyName = "CacheForTenSeconds")]
public async Task<IActionResult> GetCustomersAsync([FromQuery] string? city=null)
{
var customers = await _customerRepository.GetCustomersAsync(city);
var customerResponses = new List<GetCustomerResponse>();
foreach (var customer in customers)
{
var customerResponse = new GetCustomerResponse()
{
Id = customer.Id,
LastName = customer.LastName,
FirstName = customer.FirstName,
City = customer.City,
Email = customer.Email,
};
customerResponses.Add(customerResponse);
}

return Ok(customerResponses);
}

Cache Keys

In the context of output caching in C#, cache keys are unique identifiers associated with cached items. They are used to retrieve, store, and manage cached data. Cache keys play a crucial role in output caching as they allow developers to specify which data should be cached and provide a means to retrieve that cached data later. But if you notice we did not define any keys for caching till now, reason was by default complete URL for the endpoint will be used as Cache key for caching the resource. Now let us we got requirement to cache customers by city. We can achieve this using VaryByQuery cache key as shown below.

//Caching
builder.Services.AddOutputCache(options =>
{
// By default all the outputs will be cached for 30 seconds
options.AddBasePolicy(builder =>
builder.Expire(TimeSpan.FromSeconds(30)));

options.AddPolicy("CacheByCity", builder =>
{
builder.Expire(TimeSpan.FromSeconds(10))
.SetVaryByQuery("city");
});

////Which ever endpoint using this policy that paritcular endpoint will be cached for 10 seconds
//options.AddPolicy("CacheForTenSeconds", builder =>
// builder.Expire(TimeSpan.FromSeconds(10))
// .SetVaryByQuery("city"));
});
[HttpGet]
//[OutputCache(Duration =10)] //Cache for 10 seconds
//[OutputCache(PolicyName = "CacheForTenSeconds")]
[OutputCache(PolicyName = "CacheByCity")]
public async Task<IActionResult> GetCustomersAsync([FromQuery] string? city=null)
{
var customers = await _customerRepository.GetCustomersAsync(city);
var customerResponses = new List<GetCustomerResponse>();
foreach (var customer in customers)
{
var customerResponse = new GetCustomerResponse()
{
Id = customer.Id,
LastName = customer.LastName,
FirstName = customer.FirstName,
City = customer.City,
Email = customer.Email,
};
customerResponses.Add(customerResponse);
}

return Ok(customerResponses);
}

We can even controller the VaryByQuery at individual endpoint as well. Also, there are other ways you define your cache keys like VaryByHeader and VaryByRouteValues. Those can be achieved as well as mentioned in above example at Caching policy level as well as individual endpoint level.

Limitations

The OutputCacheOptions offer settings to control restrictions applicable across all endpoints:

  • SizeLimit — Determines the maximum size of the cache storage. Upon reaching this threshold, no new responses are cached until older entries are removed. The default is set to 100 MB.
  • MaximumBodySize — If the size of the response body surpasses this limit, caching is not performed. The default value stands at 64 MB.
  • DefaultExpirationTimeSpan — Establishes the duration for expiration when not explicitly specified by a policy. By default, this duration is set to 60 seconds.
  • Uses MemoryCache — Cached responses are stored within the process, meaning each server maintains its own cache. However, this cache is reset whenever the server process restarts.

In this blog we have seen how to configure Output caching in .NET Web API and saw its limitations. Also, we did not delve deep into other caching policies like Cache Invalidation, Cache by Location as they get very complicated. In future blog let us look at distributed caching and see how it solves some of the concepts.

Source Code for this blog can be found below:

🙏Thanks for taking the time to read the article. If you found it helpful and would like to show support, please consider:

  1. 👏👏👏👏👏👏Clap for the story and bookmark for future reference
  2. Follow me on Chaitanya (Chey) Penmetsa for more content
  3. Stay connected on LinkedIn.

Wishing you a happy learning journey 📈, and I look forward to sharing new articles with you soon.

--

--

Chaitanya (Chey) Penmetsa
CodeNx
Editor for

👨🏽‍💻Experienced and passionate software enterprise architect helping solve real-life business problems with innovative, futuristic, and economical solutions.