Caching Mechanism In ASP.NET Core


Image source: https://apisero.com/cache-scope-and-object-store-in-mule-4/

Caching refers to the process of storing frequently used data so that those data can be served much faster for any future requests. So we take the most frequently used data and copy it into temporary storage so that it can be accessed much faster in future calls from the client. If we try to explain with a simple example, Let User-1 request some data and it takes 12-15 seconds for the server to fetch the data. While fetching, we will make a copy of our fetched data parallelly to any temporary storage. So now, when User-2 requests the same data, this time we will simply serve his from the cache and it will take only 1-2 seconds for the response as we already stored the response in our cache.

There are two important terms used with cache, cache hit and cache miss. A cache hit occurs when data can be found in a cache and a cache miss occurs when data can't be found in the cache.

Caching significantly improves the performance of an application, reducing the complexity to generate content. It is important to design an application so that it never depends directly on the cached memory. The application should only cache data that don't change frequently and use the cache data only if it is available.

ASP.NET Core has many caching features. But among them the two main types are,

  • In-memory caching
  • Distributed Caching

In-memory caching

An in-memory cache is stored in the memory of a single server hosting the application. Basically, the data is cached within the application. This is the easiest way to drastically improve application performance.

The main advantage of In-memory caching is it is much quicker than distributed caching because it avoids communicating over a network and it's suitable for small-scale applications. And the main disadvantage is maintaining the consistency of caches while deployed in the cloud.

Implementing In-memory Caching with ASP.NET Core

First create an ASP.NET Core web API application.

Now inside the Startup.cs file just add the following line. This will add a non-distributed in-memory caching implementation to our application.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMemoryCache();
    //Rest of the code
}

Now let's create a new controller "EmployeeController". And in this controller we will implement our cache.

[Route("api/[controller]")]
[ApiController]
public class EmployeeController : ControllerBase
{
    private readonly IMemoryCache _memoryCache;
    private readonly ApplicationContext _context;
    public EmployeeController(IMemoryCache memoryCache, ApplicationContext context)
    {
        _memoryCache = memoryCache;
        _context = context;
    }

    [HttpGet]
    public async Task<IActionResult> GetAllEmployee()
    {
        var cacheKey = "employeeList";
        //checks if cache entries exists
        if(!_memoryCache.TryGetValue(cacheKey, out List<Employee> employeeList))
        {
            //calling the server
            employeeList = await _context.Employees.ToListAsync();

            //setting up cache options
            var cacheExpiryOptions = new MemoryCacheEntryOptions
            {
                AbsoluteExpiration = DateTime.Now.AddSeconds(50),
                Priority = CacheItemPriority.High,
                SlidingExpiration = TimeSpan.FromSeconds(20)
            };
            //setting cache entries
            _memoryCache.Set(cacheKey, employeeList, cacheExpiryOptions);
        }
        return Ok(employeeList);
    }
}

This is a pretty simple implementation. We are simply checking if any cached value is available for the specific cache key. If exists it will serve the data from the cache, if not we will call our service and save the data in the cache.

Explanation

Line 9: Injecting ImemoryCache to the constructor

Line 16: Creating a cache key. As we know that data will be saved as key-value pair.

Line 18: Checking if cache value is available for the specific key.

Line 24: Setting the cache. MemoryCacheEntryOptions is used to define crucial properties of cache. some of the properties are:

1. Priority - Priority defines the priority of keeping cache entry in the cache. The default value is set to Normal.

2. Sliding Expiration - A specific timespan within which the cache will expire if it is not used by anyone. As we set the sliding expiration to 20 seconds so it means after cache entry if there is no client request for 20 seconds the cache will be expired.

3. Absolute Expiration - It refers to the actual expiration of the cache entry without considering the sliding expiration. In our code, we set the absolute expiration to 50 seconds. So it means the cache will expire every 50 seconds for sure.

Now let's observe the performance boost of our application after implementing the In-memory caching.

For this run the application and send a get request to the web API using Postman. So the first time we send a request to our API it takes about 2061ms.

So for the first time when we call our API it directly fetches data from the database and parallelly we store the data to the cache.

Now if we request the same endpoint for the same data this time it will only take 20ms.

So this is a pretty amazing improvement. In my case, the dataset is small. If there is a big set of data on that case it will drastically improve our service.

Distributed Caching

Distributed cache is a cache that can be shared by one or more applications and it is maintained as an external service that is accessible to all servers. So distributed cache is external to the application.

The main advantage of distributed caching is that data is consistent throughout multiple servers as the server is external to the application, any failure of any application will not affect the cache server.

Here we will try to implement Distributed Caching with Redis. 

Redis is an open-source(BSD licensed), in-memory data structure store, used as a database cache and message broker. It is really fast key-value based database and even NoSQL database as well. So Redis is a great option for implementing highly available cache.

Setting up Redis in Docker

Step 1

Pull docker Redis image from docker hub.

docker pull redis

Step 2

Run redis images by mapping Redis port to our local system port.

docker run --name myrediscache -p 5003:379 -d redis

Step 3

Start the container.

docker start myrediscache

As our Redis is set up now let's go for the implementation of Distributed caching with ASP.NET Core Application.

Implementation of Distributed Cache(Redis) with ASP.NET Core

Create an ASP.NET Core Web API project and install the following library using Nuget Package Manager.

As we have already added our required package, now register the services in Startup.cs file.

public void ConfigureServices(IServiceCollection services) {
    //Rest of the code
    services.AddStackExchangeRedisCache(options => {
        options.Configuration = Configuration.GetConnectionString("Redis");
        options.InstanceName = "localRedis_";
    });
}

Here, we set "options.InstanceName" it will just act as a prefix to our key name on the redis server. Ex. if we set a cache name employeelist in the redis server it will be something like localRedis_employeelist.

And we will provide the configuration-related settings for the Redis in appsettings.json.

{
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "Redis": "localhost:5003",
    "DefaultConnection": "Data Source=.;Initial Catalog=BuildingDataDB;Integrated Security=True"
  }
}

Create a helper class "DistributedCacheExtensions" where we will Get and Set Values from and to Redis Cache.

public static class DistributedCacheExtension {
    public static async Task SetRecordAsync < T > (this IDistributedCache cache, string recodeId, T data, TimeSpan ? absoluteExpireTime = null, TimeSpan ? slidingExpirationTime = null) {
        var options = new DistributedCacheEntryOptions();
        options.AbsoluteExpirationRelativeToNow = absoluteExpireTime ?? TimeSpan.FromSeconds(60);
        options.SlidingExpiration = slidingExpirationTime;
        var jsonData = JsonSerializer.Serialize(data);
        await cache.SetStringAsync(recodeId, jsonData, options);
    }
    public static async Task < T > GetRecordAsync < T > (this IDistributedCache cache, string recordId) {
        var jsonData = await cache.GetStringAsync(recordId);
        if (jsonData is null) {
            return default (T);
        }
        return JsonSerializer.Deserialize < T > (jsonData);
    }
}

Here this code is pretty self-explanatory. In the "SetRecodeAsync" method we are saving the data to the Redis Cache. Here we have configured the IDistributedCache server with AbsoluteExpirationRelativeToNow and SlidingExpiration(Line 12 & Line 13) and we have already discussed these terms in our In-memory Caching section.

And in the "GetRecordAsync" we are getting the cached value depending on some recodeKey.

Now we will create a controller named "StudentController",

public class StudentController: ControllerBase {
    private readonly ApplicationContext _context = null;
    private readonly IDistributedCache _cache;
    public StudentController(ApplicationContext context, IDistributedCache cache) {
            _context = context;
            _cache = cache;
        }
        [HttpGet]
    public async Task < ActionResult < List < Student >>> Get() {
        var cacheKey = "GET_ALL_STUDENTS";
        List < Student > students = new List < Student > ();
        var data = await _cache.GetRecordAsync < List < Student >> (cacheKey);
        if (data is null) {
            Thread.Sleep(10000);
            data = _context.Student.ToList();
            await _cache.SetRecordAsync(cacheKey, data);
        }
        return data;
    }
}

Explanation

Line 5: Injecting the IDistributeCache in our constructor.

Line 15: creating a Cache key

Line 18: Trying to get the from the Redis cache server. If data is found on the cache server it will serve the client with the cached data.

Line 20: Checking if Cache data is available. If data are not cached then it will fetch the data from the database or other services. So to simulate it we intentionally put some delay using the Thread.Sleep() method.

So it's pretty simple.

Now let's run the application.

So for the first time, we call the Get method of the student controller it takes about 12 seconds to load the data. As for the first run, it didn't find the data in the cache so it goes to the database and fetches the data and parallelly it saves the fetched data to the Redis cache server.

But in the second run it fetches the data within 28 milliseconds. Because for the second time user request the same data application, found the data had been cached in the Redis server so it server the user with the cached data.

This is a super optimization of our application with a blazing fast speed.

So with this our journey to the introduction to Caching ends. Here I tried to keep the example as simple as possible and discussed its basic concepts. Hope you will find it helpful.

Happy coding!

While studying this topic I found some  really good and helpful articles; you can also check them out.

Reference

  1. https://medium.com/net-core/in-memory-distributed-redis-caching-in-asp-net-core-62fb33925818
  2. https://codewithmukesh.com/blog/in-memory-caching-in-aspnet-core/
  3. https://sahansera.dev/in-memory-caching-aspcore-dotnet/
  4. https://www.ezzylearning.net/tutorial/distributed-caching-in-asp-net-core-using-redis-cache