Application performance
.NET .NET Core Architecture Best Practices C# Caching Performance Visual Studio Web API

Improving Performance with Response Caching of a .NET Core Web API

Welcome to today’s post.

In today’s post I will discuss how we can use response caching in a web server to limit the number of requests that are made from a client, and to reduce the workload of the server in response generation.

In a previous post I showed how we can use in-memory caching to improve performance within a .NET Core Web API web service. In today’s post we will be using HTTP based response caching on the web server, which is enabled using the Response Caching Middleware. This will also satisfy the HTTP 1.1 caching specification. 

In the sections that follow, I will first explain what a cache-control header is, and how it is used to control response caching. I will then show how to configure response caching in a .NET Core Web API. I will then show how to test response caching in a .NET Core web API.

The Cache-Control Header

To use HTTP caching, the main header that is used is the Cache-Control header.

There are several directives that can be used to control response caching. I will describe the most useful ones here:

public

The cache will store the response.

max-age

This directive determines that a response not accepted if the response has aged the specified time in seconds. 

no-cache

This directive causes the response to not be stored for requests. The web server will then recreate the response.   

no-store

The cache cannot store the request or the response.

In addition to the Cache-Control, the following additional cache headers are useful in controlling caching:

Expires

This header field specifies the time when the response becomes stale.

Vary

The header field specifies which fields to match when retrieving a stored cache entry.

In the next section, I will show how to configure some of the above response caching properties within a .NET Core Web API.

Configuring Response Caching

To setup response caching middleware in a .NET Core API there are no additional NuGet packages to install. We just need to call middleware extension methods to activate response caching. In the ConfigureServices() method in Startup.cs we add the middleware service as shown:

public void ConfigureServices(IServiceCollection services)
{
    services.AddResponseCaching();
    …
}

In the Configure() method we add the response caching to the application pipeline as shown:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    …
    app.UseResponseCaching();
    …
}

Note: One condition that we have for using response caching middleware is that we need to use the CORS middleware before using response caching middleware.

To add response caching for each HTTP request and response call we can add the following delegate handler in Configure() just after the response caching middleware is enabled:

app.Use(async (context, next) =>
{
    context.Response.GetTypedHeaders().CacheControl = 
        new Microsoft.Net.Http.Headers.CacheControlHeaderValue()
        {
            Public = true,
            MaxAge = TimeSpan.FromSeconds(30)
        };
    
    context.Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.Vary] = 
        new string[] { "Accept-Encoding" };

    await next();
});

For any responses that require authentication, they should not be cached. We would not want responses that are returned by the caching middleware without a valid authorization token.

Testing Cached Responses

There are several ways we can test cached responses in a development or test environment. We can use CURL, Swagger API or POSTMAN.  

Whenever the API is called and the duration since the last call is less than the maximum age (in seconds), the response will be almost instantaneous.

age: 27  
cache-control: public,max-age=30  
content-length: 8531  
content-type: application/json; charset=utf-8  
date: Thu, 03 Jun 2021 13:04:23 GMT  
server: Microsoft-IIS/10.0  
vary: User-Agent  
x-powered-by: ASP.NET  
x-sourcefiles: =?UTF-8?B?QzpcZGV2Z .. ucmVz?= 

Whenever the API is called, the cached response will show the age reset back to zero.

cache-control: public,max-age=30  
content-type: application/json; charset=utf-8  
date: Thu, 03 Jun 2021 13:03:15 GMT  
server: Microsoft-IIS/10.0  
transfer-encoding: chunked  
vary: User-Agent  
x-powered-by: ASP.NET  
x-sourcefiles: =?UTF-8?B?QzpcZGV2ZWxvcG1lbnRcY2xvdWQgYXBwc1xi .. VucmVz?= 

If we set a breakpoint in our API controller and execute the API HTTP GET call:

http://localhost:25138/api/Book/Genres

Or using CURL:

curl -X GET "http://localhost:25138/api/Book/Genres" -H "accept: application/json;odata.metadata=minimal;odata.streaming=true"

Every 30 seconds our request caching middleware will be permitted to enter the API method body and regenerate the content:

[HttpGet("api/[controller]/Genres")]
[ResponseCache(VaryByHeader="User-Agent", Duration=30)]
public async Task<List<GenreViewModel>> Genres()
{
    try
    {
        var genres = await _bookService.GetGenres();
        return genres;
    }
    catch (Exception ex)
    {
        throw new GeneralException(ex.Message);
    }
}

In POSTMAN, the response duration reflects whether caching was triggered during the request and response. For the above API method, the ResponseCache attribute and corresponding parameters will assign the cache-control header field as public; max-age: 30 and the vary header field as User-Agent. In the call below we see the response takes a while for the initial un-cached call:

Before the maximum age is reached the above API controller method body will be skipped and the content stored on the web server cache will be returned to the client. The response for cached content will then be significantly reduced:

Response caching will only serve a response of the method that is generating the response. It does not cause an error or exception and returns a 200 status.

As I mentioned, for responses that are for authenticated clients, we can disable caching selectively for any API controller method. We make use of the ResponseCache attribute with the NoStore parameter set to true. Below is an example of how we can do this for a method that requires authorization using JWT authentication:

[HttpGet("api/[controller]/List")]
[ResponseCache(NoStore = true)]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<List<BookViewModel>> List()
{
    try
    {
        var books = await _bookService.GetBooks();
        return books;	
    }
    catch (Exception ex)
    {
        throw new GeneralException(ex.Message);
    }
}

The response for an authenticated API controller method will not be cached, with the response duration reflecting this, and the header showing a cache-control as no-store.

The following are some conditions for response caching:

  • Result server response must be a status code of 200.
  • Request method must be a GET.
  • Response Caching Middleware must be called before middleware that depends on caching.
  • Authorization header must not be present in the request.
  • Response must be public.
  • The Cache-Control header’s no-store directive must not be present.
  • The Set-Cookie header must not be present.
  • Vary header parameters must not equal *.
  • The response must not be stale as specified by the Expires header and the max-age cache directive.

We have seen how to use response caching to significantly increase the performance of a .NET Core Web API method. We have also seen an example of where we have applied some of the conditions of caching, such as varying the cache response by duration or the browser type and applied some conditions of when to not use caching, such as using a no store cache with methods that require authentication.

That is all for today’s post.

I hope you have found today’s post useful and informative.

Social media & sharing icons powered by UltimatelySocial