Cache-Aside Pattern Using ASP.NET Core And Azure Redis Cache

In the software development cycle, often the focus is on the performance of the application. There are many ways to improve the performance and one of the most commonly used patterns to improve the performance in modern cloud applications is the cache-aside pattern. In this post, I will describe briefly about cache-aside pattern and its implementation using ASP.NET Core.

Introduction

This pattern is fairly straightforward and its sole purpose is to load data on demand into the cache from the data source. This helps in maintaining the consistency between the data in the cache and its underlying data source.

Following are the characteristics of the pattern.

  • When an application needs data, first it will look into the cache.
  • In case the data is present in the cache, then the application will use the data from the cache.
  • Otherwise, data will be retrieved from the data source.

The below is the diagrammatic illustration

 Illustration

The cache object has to be invalidated upon changes in the value by the application.

 Application

The order of invalidating the cache is important. Update the data source before removing the item from the cache. In case, you removed the item from the cache first, there are chances the client might fetch the item before the data store is updated. That will result in data inconsistency between the data store and cache.

When to use this pattern?

  • This pattern enables us to load data on demand and can be used when the resource demand is unpredictable
  • A cache that doesn't provide read-through and write-through operations.

Note

  • Read-Through: It's a cache that sits in line with the database and in case of a cache miss, it can load the data from the database and populate the cache.
  • Write-Through: The cache sits in line with the database and data always goes through the cache to the main database.

Create Azure Resources

As illustrated above, we need the database (Azure SQL Server) and Cache (Azure Redis Cache). You can choose the database and cache of your convenience.

$resourceGroup="<Resource Group>"
$location="<location>"
$redisCacheName="<Redis cache name>"
$sqlServerName="<Azure SQL Server Name>"
$sqlDBName="<Azure SQL DB Name>"
$adminName="<admin name of SQL server>"
$adminPassword="<admin password of SQL Server>"

# Creating a resource group
az group create --name $resourceGroup --location $location

# Create Redis Cache with SKU as Basic
az redis create --name $redisCacheName --resource-group $resourceGroup --location $location --sku Basic --vm-size c0

# Create SQL Server
az sql server create -l $location -g $resourceGroup -n $sqlServerName -u $adminName -p $adminPassword

# Create SQL database with SKU as Basic
az sql db create -g $resourceGroup -s $sqlServerName -n $sqlDBName --service-objective Basic

Implementation

Let's begin with the implementation by creating an ASP.NET Core Web API project and adding Nuget packages required for Redis cache and Entity Framework Core.

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis

Firstly, let's create a country model class.

public class Country  
{  
    public int Id { get; set; }  
    public string Name { get; set; }  
    public bool IsActive { get; set; }  
}  

Now, let's register the dependencies of EF Core and Redis cache in the ConfigureServices method of the Startup class.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddDbContext<CountryContext>(optionsAction =>
        optionsAction.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddStackExchangeRedisCache(setupAction =>
    {
        setupAction.Configuration = Configuration.GetConnectionString("RedisConnectionString");
    });
}

Now modify the appsettings.json file to accommodate the connection strings of Redis Cache and SQL Database.

"ConnectionStrings": {
  "RedisConnectionString": "<Redis Cache ConnectionString>",
  "DefaultConnection": "<SQL Server Connection string>"
}

Let's add the DbContext class.

public class CountryContext : DbContext
{
    public DbSet<Country> Countries { get; set; }
    public CountryContext(DbContextOptions dbContextOptions) : base(dbContextOptions)
    {
    }
}

The GetCountries method tries to retrieve an item from the cache using a key. If the match is found, it's returned. Otherwise, the data will be retrieved from the database and populated in the cache. The cached item is configured to expire after 5 minutes.

[Route("api/[controller]")]
[ApiController]
public class CountryController : ControllerBase
{
    private readonly IDistributedCache cache;
    private readonly CountryContext countryContext;

    public CountryController(IDistributedCache cache, CountryContext countryContext)
    {
        this.cache = cache;
        this.countryContext = countryContext;
    }

    // GET: api/<CountryController>
    [HttpGet]
    public async Task<IEnumerable<Country>> GetCountries()
    {
        var countriesCache = await cache.GetStringAsync("countries");
        var value = (countriesCache == null) ? default : JsonConvert.DeserializeObject<IEnumerable<Country>>(countriesCache);
        if (value == null)
        {
            var countries = countryContext.Countries.ToList();
            if (countries != null && countries.Any())
            {
                await cache.SetStringAsync("Countries", JsonConvert.SerializeObject(countries), new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
                });
                return countries;
            }
        }
        return value;
    }
}

The AddCountries method illustrates how the cache can be invalidated upon adding/updating the data to the database.

// POST api/<CountryController>
[HttpPost]
public async Task<ActionResult<string>> AddCountries([FromBody] Country country, CancellationToken cancellationToken)
{
    if (country == null)
        return BadRequest("country is null");

    await countryContext.AddAsync(country);
    await countryContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
    await cache.RemoveAsync("countries", cancellationToken).ConfigureAwait(false);
    return Ok("cache has been invalidated");
}

Conclusion

In this article, I described the Cache-Aside pattern and its primary implementation using ASP.NET Core and Azure Redis Cache. Happy Caching!

I hope you like the article. In case you find the article interesting then kindly like and share it.