Common features in ASP.NET Core 2.2 WebApi: Caching

Common features in ASP.NET Core 2.2 WebApi: Caching

 

Introduction

In the previous article we talked about the answers to probable interrogations that we could have on the performances of a Web API by helping us with the metrics collected with MiniProfiler.
We will now see a feature allowing us to optimize the performance of our Web API: data caching.

In this article we will see four ways to cache our data:

  • In-memory cache with IMemoryCache provided by Microsoft.Extensions.Caching.Memory assembly
  • Response cache with ResponseCacheAttribute provided by Microsoft.AspNetCore.Mvc assembly
  • Global Response cache with the default Response Caching Middleware provided by Microsoft.AspNetCore.ResponseCaching assembly (won’t be described in this article)
  • Global Response cache with a custom Response Caching Middleware provided by Microsoft.AspNetCore.ResponseCaching assembly

Installation

Download this package:

PM> Install-Package Microsoft.AspNetCore.ResponseCaching

This package is required only for the global cache with middleware

The two others are provided by the default package installed when you create a new app: Microsoft.AspNetCore.App

Configuring Startup.cs 

It’s really simple to configure and activate cache, let’s see how it works (No configuration is required to activate Response cache by MVC Attribute):

First let’s add In-memory cache and Response Caching Middleware

in ConfigureServices method:

public void ConfigureServices(IServiceCollection services)
{
   // cache in memory
   services.AddMemoryCache();
   // caching response for middlewares
   services.AddResponseCaching();
}

AddResponseCaching() is required if you need to use the default Response Caching Middleware AND a custom middleware

Now let’s activate the default Response Caching Middleware:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
   // caching response for middlewares
   app.UseResponseCaching();
}

No particular activation is required for In-memory cache (IMemoryCache)

Using cache in the Web API

Let’s implement a sample of each case described in introduction:

public class DemoCachingController : ControllerBase
{
   private readonly IMemoryCache _cache;

   public DemoCachingController(IMemoryCache cache)
   {
      _cache = cache;
   }

   // GET: api/DemoCaching/memorycache
   [HttpGet("memorycache")]
   public string Get()
   {
      var cacheEntry = _cache.GetOrCreate("MyCacheKey", entry =>
      {
         entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10);
         return LongTimeOperation();
      });
      return cacheEntry;
   }

   // GET: api/DemoCaching/responsecache
   [HttpGet("responsecache")]
   [ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any)]
   public string Get2()
   {
      return LongTimeOperation();
   }

   // GET: api/DemoCaching/globalcache
   [HttpGet("globalcache")]
    public string Get3()
    {
       return LongTimeOperation();
    }

    private string LongTimeOperation()
    {
       Thread.Sleep(5000);
       return "Long time operation done!";
    }
}

In-memory cache with IMemoryCache

This use case represents a cache stored in the memory of the web server and works natively with ASP.NET Core dependency injection.

In this sample we use the method GetOrCreate that creates an entry in the server memory the first time we use it with a specific entry “MyCacheKey”, The operation result that needs to be cached is executed this time.

The second time the operation is called, GetOrCreate  method will check in the memory if an entry with the “MyCacheKey” exists, and will load it from memory. The operation that provides the result is not executed.

For the cache expiracy I recommand to use AbsoluteExpirationRelativeToNow option because the cache will expire after a precise time (up to you for the duration), instead of SlidingExpiration that expires after a precise duration if the cache has not been used. By this way you will ensure your data will be up to date after a certain moment.

Response cache with ResponseCacheAttribute

This case represents a cache stored on the server or / and on the client browser.

This attribute can be used to avoid any cache as well.

There are not so much properties:

  • Duration that indicates the time of expiracy in seconds
  • Location allow to choose where to cache your response (Any for client + browser, Client for the browser only, None to avoid any caching)
  • NoStore overrides most of the other properties. When this property is set to true, the Cache-Control header is set to no-store. If Location is set to None: Cache-Control is set to no-store,no-cache. Pragma is set to no-cache. If NoStore is false and Location is None, Cache-Control and Pragma are set to no-cache.
  • VaryByHeader stores cache for one / several header, example: VaryByHeader = “User-Agent” will cache the same data or clients that have the same User Agent.
  • VaryByQueryKeys is special, when you  want to use it, enabling the middleware for response caching is required (services.AddResponseCaching() and app.UseResponseCaching()). This option allow you to cache by one  / several QueryString key. (http://localhost:xxxxx/api/?param1=value1&param2=value2).

In the code sample above, I cache my action for 1 minute on the client and the server.

This way to cache can be used for error pages for example because the default Response Caching Middleware ignores HTTP status other than 200 (OK).

Response cache with a custom Response Caching Middleware

This way to cache allow you to cache gobally any HTTP responses.

The good news is that you can setup in the middleware QueryString keys you want to apply cache like the ReponseCacheAttribute by playing with IResponseCachingFeature Interface.

Example of a custom Response Caching Middleware:

This middleware caches all HTTP responses during 10 seconds if the URL contains the QueryString “Param1”.

public class CachingMiddleware
{
   private readonly RequestDelegate _next;

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

   public async Task Invoke(HttpContext context)
   {
      // Sample of global cache for any request that contains in QueryString "Param1"
      // Note that the middleware ignores requests doesn't return 200 (OK)
      context.Response.GetTypedHeaders().CacheControl =
      new Microsoft.Net.Http.Headers.CacheControlHeaderValue()
      {
         Public = true,
         MaxAge = TimeSpan.FromSeconds(10)
      };
      var responseCachingFeature = context.Features.Get<IResponseCachingFeature>();
      if (responseCachingFeature != null)
      {
         responseCachingFeature.VaryByQueryKeys = new[] { "Param1" };
      }
      await _next(context);
   } 
}

Demo

Let’s use Postman for the demos:

Demo with IMemoryCache

First call

Second call

Demo with ResponseCacheAttribute

First call

Second call

Demo with a custom Response Caching Middleware

First call

Second call

Note that to make everything working with Postman, just ensure to disable “Send no-cache header”

Conclusion

You saw how to use cache with 3 differents manners.

Keep in mind caching can significantly improve the performance and scalability of an ASP.NET Core app.

There is also another one manner you can use not described in this article: Distributed cache, this is a different cache because this cache is shared by multiple app servers, typically maintained as an external service to the app servers that access it. I will talk about this kind of cache in another article.

Hope you enjoyed this article 🙂