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 pattern to improve the performance in modern cloud application is Cache-aside pattern. In this post, I will describe briefly about Cache-aside pattern and it’s implementation using ASP.NET Core.

Introduction

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

Following are the characteristics of the pattern

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

The below is the diagrammatic illustration

The cache object has to be invalidated upon changes in the value by the 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 cache first, there are chances of client might fetch the item before the data store is updated. That will result in data inconsistency between 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 cache miss, it can load the data from the database and populates 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 add 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 ConfigureServices method of 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 connectionstrings of Redis Cache and SQL Database

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

Lets add 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 populate it to 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 illustrate 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 Cache-Aside pattern and it’s primary implementation using ASP.NET Core and Azure Redis Cache. Happy Caching

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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s