How To Implement Caching In The .NET Core Web API Application

Introduction

Performance is one of the important factor of the application. There are many factors (such as DB call (querying), Web service load, etc) that impact the application performance. We have to more concentrate on the performance side for delivering the quality application. For that, one of the options is called cache. In this article, we are going to explore what is cache and how to implement caching in .NET Core web API application.

What is Cache?

Caching is the technique of storing the data which are frequently used. Those data can be served faster for any future or subsequent requests by eliminating the unnecessary requests to external data sources.

Why do we need cache mechanism?

Sometimes our application frequently calls the same method and fetch the data from the database. The output of these requests is the same at all times. It doesn't get changed or updated in the database. In this case, we can use caching to reduce the database calls and retrieve the data directly from the cache memory.

There are 3 types of cache available,

  1. In-Memory Cache - The data are cached in the server's memory.
  2. Persistent in-process Cache - The data are cached in some file or database.
  3. Distributed Cache - The data are cached in shared cache and multiple processes. Example: Redis cache

In this article, we are mainly focusing on In-Memory Cache mechanism.

In-Memory Cache

In-Memory cache means storing the cache data on the server's memory.

Pros

  • It is easier and quicker than other caching mechanisms
  • Reduce load on Web Services/ Database
  • Increase Performance
  • Highly Reliable
  • It is suited for small and middle applications.

Cons

  • If the cache is not configured properly then, it can consume the server’s resources.
  • Increased Maintenance.
  • Scalability Issues. It is suitable for single server. If we have many servers then, can't share the cache to all servers.

How to implement In-Memory cache in the ASP.NET Core Web API application

Prerequisites

  • Visual Studio 2019 or Visual Studio 2022

Follow the below steps to create the ASP.NET Web API using Visual Studio 2019.

Step 1

Open Visual Studio 2019, click Create a new project.

Step 2

Select ASP.NET Core Web Application project template and click Next.

Step 3

Enter the project name as Sample_Cache and Click Next.

Step 4

Select .NET Core 3.1 and Empty project and Click Create.

Step 5

Install the Microsoft.Extensions.Caching.Memory NuGet Package to implement the in-memory cache.

Step 6

Create EmployeeController class and inject the in-memory caching service into the constructor.

public class EmployeeController: ControllerBase {
    private ICacheProvider _cacheProvider;
    public EmployeeController(ICacheProvider cacheProvider) {
        _cacheProvider = cacheProvider;
    }
}

Step 7

Register services.AddMemoryCache() to ConfigureServices() method in the Startup class. 

public void ConfigureServices(IServiceCollection services)
{
	services.AddMemoryCache();
}

Simple Cache Implementation

Let us add the below code in the EmployeeController class.

EmployeeController.cs

[Route("getAllEmployee")]
public IActionResult GetAllEmployee() {
    if (!_cache.TryGetValue(CacheKeys.Employees, out List < Employee > employees)) {
        employees = GetEmployeesDeatilsFromDB(); // Get the data from database
        var cacheEntryOptions = new MemoryCacheEntryOptions {
            AbsoluteExpiration = DateTime.Now.AddMinutes(5),
                SlidingExpiration = TimeSpan.FromMinutes(2),
                Size = 1024,
        };
        _cache.Set(CacheKeys.Employees, employees, cacheEntryOptions);
    }
    return Ok(employees);
}

 Employee.cs

public class Employee
{
	public int Id { get; set; }
	[Required]
	public string FirstName { get; set; }
	[Required]
	public string LastName { get; set; }
	[Required]
	public string EmailId { get; set; }
}

CacheKeys.cs

public static class CacheKeys
{
	public static string Employees => "_Employees";
}

In the above code, whenever the request is coming, first check whether the cache has value (employee details) or not. If not, then employees details are retrieved from the database and stored in the cache. Since the cache has employee details, So for the next request the employee details are fetched from the cache instead of the database.

In-Memory Cache Parameters

  • Size - This allows you to set the size of this particular cache entry, so that it doesn’t start consuming the server resources.
  • SlidingExpiration - How long the cache will be inactive. A cache entry will expire if it is not used by anyone for this particular time period. In our case, we have set SlidingExpiration is 2 minutes. If no requests for this cache entry for 2 minutes, then the cache will be deleted.
  • AbsoluteExpiration - Actual expiration time for cached value. Once the time is reached, then the cache entry will be removed. In our case, we have set AbsoluteExpiration is 5 minutes. The cache will be expired once the time is reached 5 minutes. Absolute expiration should not less than the Sliding Expiration.

Output

First time Request 

How to implement caching in the .NET Core Web API Application

Second Request

How to implement caching in the .NET Core Web API Application

Problems

If there are multiple requests at the same time for the same item, then the requests won't wait for the first one to be complete. Since there are no values in the cache, the database calls happen to all requests until cached has been set. 

For example, 

  1. User 'A' sends request for the employee details. Here cache doesn't have any values. So, this request calls the database for retrieving the employee details. Let’s consider that getting employee details from database takes 5 seconds. 
  2. Meanwhile (Before completing the User 'A' request), User 'B' sends request for the same employee details. Here the cache doesn't have any value. So, this request also has a database call.

Cache Implementation using lock

We are going to use the lock to solve the above problem.

For example, 

  1. User 'A' sends the request for the employee details. Here cache doesn't have any values. So, it enters into the lock state and makes database calls for retrieving the employee details.
  2. Meanwhile, User 'B' sends the request for the employee details. This request will wait until the lock is released. This will reduce the database call for the same data.
  3. Once employee details are retrieved from the database, it's stored in the cache. Now, the lock is released and sends a response to the User 'A'.
  4. Now, the values are fetched from the cache and sent a response to the User 'B'.

We are going to refactor the cache mechanism using locks. Let consider the below code.

EmployeeController.cs

[Route("api/[controller]")]
public class EmployeeController: ControllerBase {
    private ICacheProvider _cacheProvider;
    public EmployeeController(ICacheProvider cacheProvider) {
            _cacheProvider = cacheProvider;
        }
        [Route("getAllEmployee")]
    public IActionResult GetAllEmployee() {
        try {
            var employees = _cacheProvider.GetCachedResponse().Result;
            return Ok(employees);
        } catch (Exception ex) {
            return new ContentResult() {
                StatusCode = 500,
                    Content = "{ \n error : " + ex.Message + "}",
                    ContentType = "application/json"
            };
        }
    }
}

CacheProvider.cs

public class CacheProvider: ICacheProvider {
    private static readonly SemaphoreSlim GetUsersSemaphore = new SemaphoreSlim(1, 1);
    private readonly IMemoryCache _cache;
    public CacheProvider(IMemoryCache memoryCache) {
        _cache = memoryCache;
    }
    public async Task < IEnumerable < Employee >> GetCachedResponse() {
        try {
            return await GetCachedResponse(CacheKeys.Employees, GetUsersSemaphore);
        } catch {
            throw;
        }
    }
    private async Task < IEnumerable < Employee >> GetCachedResponse(string cacheKey, SemaphoreSlim semaphore) {
        bool isAvaiable = _cache.TryGetValue(cacheKey, out List < Employee > employees);
        if (isAvaiable) return employees;
        try {
            await semaphore.WaitAsync();
            isAvaiable = _cache.TryGetValue(cacheKey, out employees);
            if (isAvaiable) return employees;
            employees = EmployeeService.GetEmployeesDeatilsFromDB();
            var cacheEntryOptions = new MemoryCacheEntryOptions {
                AbsoluteExpiration = DateTime.Now.AddMinutes(5),
                    SlidingExpiration = TimeSpan.FromMinutes(2),
                    Size = 1024,
            };
            _cache.Set(cacheKey, employees, cacheEntryOptions);
        } catch {
            throw;
        } finally {
            semaphore.Release();
        }
        return employees;
    }
}

Explanation of the code

First, we are checking if the cache has value or not.

bool isAvailable = _cache.TryGetValue(cacheKey, out List<Employee> employees);

if (isAvailable)
	return employees;

If the value is available in the cache, then getting the value from the cache and send response to the user else then asynchronously wait to enter the Semaphore.

await semaphore.WaitAsync();

Once a thread has been granted access to the Semaphore, recheck if the value has been populated previously for safety (Avoid concurrent thread access).

isAvailable = _cache.TryGetValue(cacheKey, out employees);

if (isAvailable)
	return employees;

Still don’t have a value in the cache then, call the database and store the value in the cache.

employees = EmployeeService.GetEmployeesDeatilsFromDB();

var cacheEntryOptions = new MemoryCacheEntryOptions
{
	AbsoluteExpiration = DateTime.Now.AddMinutes(5),
	SlidingExpiration = TimeSpan.FromMinutes(2),
	Size = 1024,
};

_cache.Set(cacheKey, employees, cacheEntryOptions);

Send response to the user.

Summary

In this article, we have learned the following functionality using .NET Core Web API. 

  • What is Caching, 
  • Types of Caching mechanism
  • Detail about In-Memory Caching
  • Implementing In-Memory Caching in ASP.NET Core Web API.