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 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
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 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 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 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.
  1. $resourceGroup="<Resource Group>"  
  2. $location="<location>"  
  3. $redisCacheName="<Redis cache name>"  
  4. $sqlServerName="<Azure SQL Server Name>"  
  5. $sqlDBName="<Azure SQL DB Name>"  
  6. $adminName="<admin name of SQL server>"  
  7. $adminPassword="<admin password of SQL Server>"  
  8.   
  9. //Creating a resource group  
  10. az group create --name $resourceGroup --location $location  
  11.   
  12. //Create Redis Cache with SKU as Basic  
  13. az redis create --name $redisCacheName --resource-group $resourceGroup --location $location --sku Basic --vm-size c0  
  14.   
  15. //Create SQL Server  
  16. az sql server create -l $location -g $resourceGroup -n $sqlServerName -u $adminName -p $adminPassword  
  17.   
  18. //Create SQL database with SKU as Basic  
  19. 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.
  1. dotnet add package Microsoft.EntityFrameworkCore.SqlServer  
  2. dotnet add package Microsoft.EntityFrameworkCore.Tools  
  3. dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis  
Firstly, let's create a country model class.
  1. public class Country  
  2.     {  
  3.         public int Id { getset; }  
  4.         public string Name { getset; }  
  5.         public bool IsActive { getset; }  
  6.     }  
Now, let's register the dependencies of EF Core and Redis cache in ConfigureServices method of Startup class.
  1. public void ConfigureServices(IServiceCollection services)  
  2.         {  
  3.             services.AddControllers();  
  4.             services.AddDbContext<CountryContext>(optionsAction =>   
  5.                   optionsAction.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));  
  6.   
  7.             services.AddStackExchangeRedisCache(setupAction =>  
  8.             {  
  9.                 setupAction.Configuration= Configuration.GetConnectionString("RedisConnectionString");  
  10.             });  
  11.         }  
Now modify the appsettings.json file to accommodate the connectionstrings of Redis Cache and SQL Database
  1. "ConnectionStrings": {  
  2.     "RedisConnectionString""<Redis Cache ConnectionString>",  
  3.     "DefaultConnection""<SQL Server Connection string>"  
  4.   }  
Let's add DbContext class
  1. public class CountryContext:DbContext  
  2.     {  
  3.         public DbSet<Country> Countries { getset; }  
  4.         public CountryContext(DbContextOptions dbContextOptions):base(dbContextOptions)  
  5.         {  
  6.         }  
  7.     }  
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 the cache. The cached item is configured to expire after 5 minutes.
  1. [Route("api/[controller]")]  
  2.     [ApiController]  
  3.     public class CountryController : ControllerBase  
  4.     {  
  5.         private readonly IDistributedCache cache;  
  6.         private readonly CountryContext countryContext;  
  7.   
  8.         public CountryController(IDistributedCache cache,CountryContext countryContext)  
  9.         {  
  10.             this.cache = cache;  
  11.             this.countryContext = countryContext;  
  12.         }  
  13.   
  14.         // GET: api/<CountryController>  
  15.         [HttpGet]  
  16.         public async Task< IEnumerable<Country>> GetCountries()  
  17.         {  
  18.             var countriesCache = await cache.GetStringAsync("countries");  
  19.             var value= (countriesCache == null)? default  
  20.                 : JsonConvert.DeserializeObject<IEnumerable< Country>>(countriesCache);  
  21.             if (value == null)  
  22.             {  
  23.                 var countries=countryContext.Countries.ToList();  
  24.                 if(countries!=null && countries.Any())  
  25.                 {  
  26.                     await cache.SetStringAsync("Countries", JsonConvert.SerializeObject(countries), new DistributedCacheEntryOptions  
  27.                     {  
  28.                         AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)  
  29.                     });  
  30.                     return countries;  
  31.                 }  
  32.             }  
  33.             return value;  
  34.         }  
  35. }  
The AddCountries method illustrates how the cache can be invalidated upon adding/updating the data to the database.
  1. // POST api/<CountryController>  
  2.         [HttpPost]  
  3.         public async Task<ActionResult<string>> AddCountries([FromBody] Country country, CancellationToken cancellationToken)  
  4.         {  
  5.             if (country == null)  
  6.                 return BadRequest("country is null");  
  7.   
  8.             await countryContext.AddAsync(country);  
  9.             await countryContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);  
  10.             await cache.RemoveAsync("countries", cancellationToken).ConfigureAwait(false);  
  11.             return Ok("cache has been invalidated");  
  12.         }  

Conclusion

 
In this article, I described 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.