Application performance
.NET .NET Core ASP.NET Core C# Caching Performance Visual Studio Web API

How to Improve Performance with Output Caching in ASP.NET Core 7.0

Welcome to today’s post.

In today’s post I will be explaining what output caching is within a web application, how it differs from response caching, when to make use of it, and give examples on how output caching is used within an ASP.NET Core web API application.

The caching of responses within a web application is a technique that is used to improve the performance of the application. When a web API method generates a response back to a web client, the metric we use to measure the performance is the duration of the response generation from the API method. When this duration of responses is consistently higher for all requests, the issue of application performance surfaces.

What is response and output caching?

Response caching is used by the client (usually the web browser) to control the caching of responses from an external site. It does this by adding an HTTP header into the browser response.

Output caching is used by the server to control the caching of responses to a web client. It does this by defining the caching rules on the server. The web client does not override the caching rules from the server.  

I will explain some differences between output caching and response caching.

Differences between response caching and output caching

Response caching and output caching essentially achieve the same outcome, and that is to cache content before returning it to the calling client request.

With output caching, the caching of responses is controlled on the web server. Response caching, however, adds HTTP cache headers into the client browser response. HTTP responses are not stored at the server with response caching. Output caching does not use a location as the caching is controlled only from the web server, but response caching can occur at the client (web browser or web client), server (web server or web proxy) or on both locations.

Situations when response caching should be utilized

When we have an API method that returns static data that rarely changes, then there is a need to cache the data in the response payload so that it does not need to be re-generated on every response.

When an API method returns dynamic data that varies with almost every request such as a public web site, there is no need to cache the response. If we were to apply caching in this case, web client applications and browsers would receive repeated copies of data in web responses, which may not be what users are expecting for most content.

When an API method returns dynamic data that varies by the input parameters, the caching of responses would vary by the input parameters. These can include record keys, geographic locations, categories of data, and so on.  When an API method is protected by authentication, the data returned in the response is usually private to a user. In this case, the use of response caching is ill advised. We do not want to be in a situation where the response data from a user is visible to other users. This would be a security leak!

What caching properties can be used to control caching?

In this section, I will explain the two properties that are used to control caching: Caching by duration, and Caching by parameters.

Caching by duration

When commonly accessed content from a web service is requested frequently, the performance of client applications and the backend server that is a dependency of the web API methods will be impacted with consistently high disk I/O and CPU. To remediate this situation, we can apply caching with a duration (or maximum age). From the commencement and before the expiration of the duration, all responses are cached. This allows a lot of content that rarely changes to be returned to clients without expending significant stress on the backend web service.

Caching by parameters

When response content varies by input parameters, the application of caching is not achieved solely by duration. If the content generated by geographic location parameters yielded the same data consistently (such as the cities within a state or province), then applying caching by duration would make sense. If the number of input parameters possible is finite, then we can apply an additional cache key of the location (state, province) parameter.  This allows multiple keys values to be cached by duration.

If the content generated by identification parameters yields data that can change quite often (such as the recent loans for a book or the for the books currently located in a library shelf location), then the use of duration makes less sense as the response data can change quite often. In this case the use of a cached key and very large duration makes sense.

In the next section, I will show how to configure output caching within an ASP.NET Core web application.

Configuration of output caching in an ASP.NET Core web application

Output caching has been added as a feature in ASP.NET Core 7.0. I will explain how to configure output caching during application startup. 

In the Startup class within the ConfigureServices() method, we setup output caching services in the service collection as shown:

public void ConfigureServices(IServiceCollection services)
{
    ..		
    // Enable output caching middleware.
    services.AddOutputCache();
    ..
}

Note that if we add the AddCors() service, then AddOutputCache() must be added afterwards.

In the Configure() method, we setup output caching services in the application middleware pipeline as shown:

public void Configure(IApplicationBuilder app, IHostEnvironment env, ILogger<Startup> logger)
	..
        app.UseOutputCache();
      ..
}

If we do not configure any output caching policies in our API methods or controllers, then the default caching rules apply caching to HTTP responses with a status of 200 (Ok) and HTTP GET and HEAD responses. By default, cookies within responses and authenticated requests are not cached.

In the next section, I will show how to cache an API method that caches outputs by duration.

Example of an API method that caches output by duration

In the controller API method below, we cache output for 30 seconds. During the 30 second interval the output remains unchanged.

After the duration lapses, future calls can result in changed output from the API method.

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

When we the execute a request with cached output to the above API with the following URL:

http://localhost:25138/api/Book/Genres
(With CURL the request is: 

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

The response header is shown:

 cache-control: public,max-age=30 
 content-encoding: br 
 content-type: application/json; charset=utf-8 
 date: Tue,02 May 2023 13:31:20 GMT 
 server: Microsoft-IIS/10.0 
 transfer-encoding: chunked 
 vary: User-Agent,Accept-Encoding 
 x-powered-by: ASP.NET 
 x-sourcefiles: =?UTF-8?B?Qzpc… 

The output response timing (with a 200 status) that we get from the console debug output is:

[23:31:20 INF] HTTP GET /api/Book/Genres responded 200 in 2722.3699 ms
Response sent: http://localhost:25138/api/Book/Genres with HTTP status 200.0

We can see that the first call was not cached and its output was generated by a backend service SQL  query:

[23:45:51 INF] Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [g].[ID], [g].[DateCreated], [g].[DateUpdated], [g].[Description], [g].[IsFiction], [g].[IsNonFiction]
FROM [Genres] AS [g]

When subsequent runs are executed within a few seconds, we notice a dramatic reduction in the response duration.

[23:37:18 INF] HTTP GET /api/Book/Genres responded 200 in 38.7137 ms Response sent: http://localhost:25138/api/Book/Genres with HTTP status 200.0

While the requests are submitted within the duration window before expiry, the same HTTP response is returned by the web server.

If the same request is submitted after the duration expiry is reached, the HTTP response is reset as the response is no longer cached. Below we notice the HTTP response over 8 minutes after the initial request has a different time stamp:

[23:45:51 INF] HTTP GET /api/Book/Genres responded 200 in 14.3410 ms
Response sent: http://localhost:25138/api/Book/Genres with HTTP status 200.0

When the timestamp of the output cache response changes, a call to the backend services that generate the data are also called.

Example of an API method that caches output by a parameter

The following API method sets output caching to change the output when the query key, id, of the requested data changes. The duration is large so that the cached output varies only by the key.

[HttpGet("api/[controller]/Details/{id}")]
[OutputCache(Duration = int.MaxValue, VaryByQueryKeys = new[] { "id" })]
public async Task<ActionResult> Details(int id)
{
    if (id == 0)
    {
        return NotFound(new { id });
    }
    try
    {
        BookStatusViewModel bvm = new BookStatusViewModel();
        var book = await _db.Books.Where(a => a.ID == id).SingleOrDefaultAsync();
        if (book != null)
        {
            bvm.ID = book.ID;
            ..
        }
        return Ok(bvm);
    }
    catch (Exception ex)
    {
        return BadRequest(new { ex.Message });
    }
}

When we the submit a request with cached output to the above API with the following URL:

http://localhost:25138/api/Book/Details/1
(With CURL the request is: 

curl -X 'GET' \
  'http://localhost:25138/api/Book/Details/1' \
  -H 'accept: */*'
)

The response header is shown:

cache-control: public,max-age=2147483647 
 content-encoding: br 
 content-type: application/json; charset=utf-8 
 date: Tue,02 May 2023 13:34:43 GMT 
 server: Microsoft-IIS/10.0 
 transfer-encoding: chunked 
 vary: Accept-Encoding 
 x-powered-by: ASP.NET 
 x-sourcefiles: =?UTF-8?B?Qz… 

The output response timing (with a 200 status) that we get from the console debug output is:

[23:34:43 INF] HTTP GET /api/Book/Details/1 responded 200 in 1197.2917 ms 
Response sent: http://localhost:25138/api/Book/Details/1 with HTTP status 200.0

When we submit a request with cached output to the above API with the following URL which has a different id key:

http://localhost:25138/api/Book/Details/2

the output response timing (with a 200 status) that we get from the console debug output is:

Response sent: http://localhost:25138/api/Book/Details/2 with HTTP status 200.0
[23:40:17 INF] HTTP GET /api/Book/Details/2 responded 200 in 17.5761 ms

The response header returned has a different time stamp:

cache-control: public,max-age=2147483647 
 content-encoding: br 
 content-type: application/json; charset=utf-8 
 date: Tue,02 May 2023 13:40:17 GMT 
 server: Microsoft-IIS/10.0 
 vary: Accept-Encoding 
 x-powered-by: ASP.NET 
 x-sourcefiles: =?UTF-8?B?Qz…

In each of the above responses, the following backend query was executed within the API method:

[23:42:19 INF] Executed DbCommand (2ms) [Parameters=[@__id_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [b].[ID], [b].[Author], [b].[DateCreated], [b].[DateUpdated], [b].[Edition], [b].[Genre], [b].[ISBN], [b].[Location], [b].[MediaType], [b].[Title], [b].[YearPublished]
FROM [Books] AS [b]
WHERE [b].[ID] = @__id_0

What we observe from the above is that when the same cached request is re-submitted, when the duration has not expired, the result is cached, and response is immediate.

When a different request that varies by the id is submitted, a different response header is returned for the initial unique request.

When the same request id is repeatedly sent, there are no further response headers returned as they are cached by the server.

Comparison with API methods that do not cache output response

In the first API method, the output cache attribute was removed, API service application rebuilt and re-run. The results are as follows:

When we the execute a request with cached output to the above API with the following URL:

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

The following response header is shown:

content-encoding: br 
 content-type: application/json; charset=utf-8 
 date: Tue,02 May 2023 13:58:50 GMT 
 server: Microsoft-IIS/10.0 
 transfer-encoding: chunked 
 vary: Accept-Encoding 
 x-powered-by: ASP.NET 
 x-sourcefiles: =?UTF-8?B?Qz… 

The non-cached output response timing (with a 200 status) that we get from the console debug output is:

[23:58:50 INF] HTTP GET /api/Book/Genres responded 200 in 1628.5246 ms
Response sent: http://localhost:25138/api/Book/Genres with HTTP status 200.0

This is quicker than the initial response from the cached equivalent. This is likely to be due to query response from the SQL backend.

The response header returned has a different time stamp:

content-encoding: br 
 content-type: application/json; charset=utf-8 
 date: Tue,02 May 2023 14:00:32 GMT 
 server: Microsoft-IIS/10.0 
 transfer-encoding: chunked 
 vary: Accept-Encoding 
 x-powered-by: ASP.NET 
 x-sourcefiles: =?UTF-8?B?Qz.. 

The output non-cached response timing (with a 200 status) that we get from the console debug output has a changed time stamp:

[00:00:32 INF] HTTP GET /api/Book/Genres responded 200 in 183.7583 ms
Response sent: http://localhost:25138/api/Book/Genres with HTTP status 200.0

In the second API method with the output cache attribute removed, the results are as follows:

When we the execute a request with non-cached output to the above API with the following URL:

http://localhost:25138/api/Book/Details/1

The first response header shows the following cache control and timestamp:

cache-control: public,max-age=2147483647 
…
date: Tue,02 May 2023 13:34:43 GMT 

The second response header shows the following cache control and timestamp:

cache-control: public,max-age=2147483647  
 …
date: Tue,02 May 2023 13:40:17 GMT 

The response headers have been regenerated but there are no HTTP responses. This is likely to be due to the cache control location by default being public, which is occurring on the client or web proxy (middleware). In addition, the duration, when not specified is set to a maximum possible value.

The above discussion and observations show us how to use output caching in ASP.Net Core 7.0. I showed how to apply the duration property to control the expiration time for output response. I then showed how to manage cache storage, include query keys to control caching within API methods, then manage cache keys including the insertion and eviction (removal) of keys. 

That is all for today’s post.

I hope you have found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial