This article discusses the Azure Redis Cache basics and their implementation using .NET Core Web API.
Agenda
- Introduction
- What is a Cache?
- Types of Cache
- Redis Cache
- Azure Redis Cache Setup
- Step-by-step Implementation
Prerequisites
- Visual Studio 2022
- Azure Account
- .NET Core 6
Introduction
Caching is very popular nowadays in the software industry because it will improve the performance and scalability of the application. We use many web applications like Gmail and Facebook and see how responsive they are and we have a great user experience. There are a lot of users using the internet and if an application has huge network traffic and demand, we need to take care of many things which help us to improve the performance and responsiveness of the application. So, because of that, there is the solution of caching and that’s why caching comes into the picture.
What is a Cache?
The cache is the memory storage that is used to store the frequent access data into the temporary storage, it will improve the performance drastically and avoid unnecessary database hit and store frequently used data into the cache.
As you see in the above image there are two scenarios, one is without using the cache and another is with the cache. So here when we do not use the cache, in that case, suppose users want data then they will hit each time database and it will increase the time complexity and reduce performance in case there is some static data that users want and it is the same for all users. When we do not use the cache, each one hits the unnecessary database to fetch data. On the other side. you can see we use the cache, and in that, the case if there is the same static and the same data for all users then only the first user will hit the database and fetch data and store it into the cache memory, and then other two users used that from the cache without unnecessarily hit database to fetch data.
Types of Cache
Basically, there are two types of caching .NET Core supports
- In-Memory Caching
- Distributed Caching
When we use In-Memory Cache, data is stored in the application server memory; whenever we need it, we fetch data from that and use it wherever we need it. And in Distributed Caching there are many third-party mechanisms like Redis and many others. But in this section, we look into the Redis Cache in detail and how it works in the .NET Core
Distributed Caching
- Basically, in distributed caching data are stored and shared between multiple servers
- Also, it’s easy to improve the scalability and performance of the application after managing the load between multiple servers when we use a multi-tenant application
- Suppose, in the future, if one server is crashed and restarts then the application does not have any impact because multiple servers are as per our need if we want
Redis is the most popular cache which is used by many companies nowadays to improve the performance and scalability of the application. So, we are going to discuss Redis and its usage one by one.
Redis Cache
- Redis is an Open Source (BSD Licensed) in-memory Data Structure store used as a database.
- Basically, it is used to store the frequently used and some static data inside the cache and use and reserve that as per user requirement.
- There are many data structures present in the Redis that we can use like List, Set, Hashing, Stream, and many more to store the data.
Azure Redis Cache Setup
Step 1
Log in to the Azure portal.
Step 2
Search Azure Cache for Redis in the marketplace and open it.
Step 3
Click on create and provide the other information.
Step 4
After that go to the Access keys section inside the cache which we created earlier and copy the primary connection string that we need inside the .NET Core Web API.
Step-by-step Implementation
Step 1
Open the visual studio and create a new .NET Core Web API project.
Step 2
Configure a new project.
Step 3
Provide additional details.
Step 4
Project Structure.
Step 5
Create the product details class.
namespace AzureRedisCacheDemo.Models {
public class ProductDetails {
public int Id {
get;
set;
}
public string ProductName {
get;
set;
}
public string ProductDescription {
get;
set;
}
public int ProductPrice {
get;
set;
}
public int ProductStock {
get;
set;
}
}
}
Step 6
Next, add Db Context Class inside the Data folder.
using AzureRedisCacheDemo.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
namespace AzureRedisCacheDemo.Data {
public class DbContextClass: DbContext {
public DbContextClass(DbContextOptions < DbContextClass > options): base(options) {}
public DbSet < ProductDetails > Products {
get;
set;
}
}
}
Step 7
After that, add the Seed Data class which we use to insert some data initially.
using AzureRedisCacheDemo.Models;
using Microsoft.EntityFrameworkCore;
namespace AzureRedisCacheDemo.Data
{
public class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new DbContextClass(
serviceProvider.GetRequiredService<DbContextOptions<DbContextClass>>()))
{
if (context.Products.Any())
{
return;
}
context.Products.AddRange(
new ProductDetails
{
Id = 1,
ProductName = "IPhone",
ProductDescription = "IPhone 14",
ProductPrice = 120000,
ProductStock = 100
},
new ProductDetails
{
Id = 2,
ProductName = "Samsung TV",
ProductDescription = "Smart TV",
ProductPrice = 400000,
ProductStock = 120
});
context.SaveChanges();
}
}
}
}
Step 8
Configure the Azure Redis Cache connection string inside the appsettings.json file.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"RedisURL": "<valuefromportal>"
}
Step 9
Create Configuration Manager and Connection Helper class inside the Helper folder which we use for connection purposes.
Configuration Manager
namespace AzureRedisCacheDemo.Helper {
static class ConfigurationManager {
public static IConfiguration AppSetting {
get;
}
static ConfigurationManager() {
AppSetting = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").Build();
}
}
}
Connection Helper
using StackExchange.Redis;
namespace AzureRedisCacheDemo.Helper {
public class ConnectionHelper {
static ConnectionHelper() {
ConnectionHelper.lazyConnection = new Lazy < ConnectionMultiplexer > (() => {
return ConnectionMultiplexer.Connect(ConfigurationManager.AppSetting["RedisURL"]);
});
}
private static Lazy < ConnectionMultiplexer > lazyConnection;
public static ConnectionMultiplexer Connection {
get {
return lazyConnection.Value;
}
}
}
}
Step 10
Next, add IProductService inside the Repositories.
using AzureRedisCacheDemo.Models;
namespace AzureRedisCacheDemo.Repositories {
public interface IProductService {
public Task < List < ProductDetails >> ProductListAsync();
public Task < ProductDetails > GetProductDetailByIdAsync(int productId);
public Task < bool > AddProductAsync(ProductDetails productDetails);
public Task < bool > UpdateProductAsync(ProductDetails productDetails);
public Task < bool > DeleteProductAsync(int productId);
}
}
Step 11
Next, create ProductService class and implement the IProductService interface inside that.
using AzureRedisCacheDemo.Data;
using AzureRedisCacheDemo.Models;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace AzureRedisCacheDemo.Repositories {
public class ProductService: IProductService {
private readonly DbContextClass dbContextClass;
public ProductService(DbContextClass dbContextClass) {
this.dbContextClass = dbContextClass;
}
public async Task < List < ProductDetails >> ProductListAsync() {
return await dbContextClass.Products.ToListAsync();
}
public async Task < ProductDetails > GetProductDetailByIdAsync(int productId) {
return await dbContextClass.Products.Where(ele => ele.Id == productId).FirstOrDefaultAsync();
}
public async Task < bool > AddProductAsync(ProductDetails productDetails) {
await dbContextClass.Products.AddAsync(productDetails);
var result = await dbContextClass.SaveChangesAsync();
if (result > 0) {
return true;
} else {
return false;
}
}
public async Task < bool > UpdateProductAsync(ProductDetails productDetails) {
var isProduct = ProductDetailsExists(productDetails.Id);
if (isProduct) {
dbContextClass.Products.Update(productDetails);
var result = await dbContextClass.SaveChangesAsync();
if (result > 0) {
return true;
} else {
return false;
}
}
return false;
}
public async Task < bool > DeleteProductAsync(int productId) {
var findProductData = dbContextClass.Products.Where(_ => _.Id == productId).FirstOrDefault();
if (findProductData != null) {
dbContextClass.Products.Remove(findProductData);
var result = await dbContextClass.SaveChangesAsync();
if (result > 0) {
return true;
} else {
return false;
}
}
return false;
}
private bool ProductDetailsExists(int productId) {
return dbContextClass.Products.Any(e => e.Id == productId);
}
}
}
Step 12
Create an IRedisCache interface.
namespace AzureRedisCacheDemo.Repositories.AzureRedisCache {
public interface IRedisCache {
T GetCacheData < T > (string key);
bool SetCacheData < T > (string key, T value, DateTimeOffset expirationTime);
object RemoveData(string key);
}
}
Step 13
After that, create a RedisCache class and implement the interface method inside that which we created earlier.
using AzureRedisCacheDemo.Helper;
using Newtonsoft.Json;
using StackExchange.Redis;
namespace AzureRedisCacheDemo.Repositories.AzureRedisCache
{
public class RedisCache : IRedisCache
{
private IDatabase _db;
public RedisCache()
{
ConfigureRedis();
}
private void ConfigureRedis()
{
_db = ConnectionHelper.Connection.GetDatabase();
}
public T GetCacheData<T>(string key)
{
var value = _db.StringGet(key);
if (!string.IsNullOrEmpty(value))
{
return JsonConvert.DeserializeObject<T>(value);
}
return default;
}
public object RemoveData(string key)
{
bool _isKeyExist = _db.KeyExists(key);
if (_isKeyExist == true)
{
return _db.KeyDelete(key);
}
return false;
}
public bool SetCacheData<T>(string key, T value, DateTimeOffset expirationTime)
{
TimeSpan expiryTime = expirationTime.DateTime.Subtract(DateTime.Now);
var isSet = _db.StringSet(key, JsonConvert.SerializeObject(value), expiryTime);
return isSet;
}
}
}
Step 14
Create a new Products Controller.
using AzureRedisCacheDemo.Models;
using AzureRedisCacheDemo.Repositories;
using AzureRedisCacheDemo.Repositories.AzureRedisCache;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
namespace AzureRedisCacheDemo.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
private readonly IRedisCache _redisCache;
public ProductsController(IProductService productService, IRedisCache redisCache)
{
_productService = productService;
_redisCache = redisCache;
}
/// <summary>
/// Product List
/// </summary>
/// <returns></returns>
[HttpGet]
public async Task<ActionResult<List<ProductDetails>>> ProductListAsync()
{
var cacheData = _redisCache.GetCacheData<List<ProductDetails>>("product");
if (cacheData != null)
{
return new List<ProductDetails>(cacheData);
}
var productList = await _productService.ProductListAsync();
if(productList != null)
{
var expirationTime = DateTimeOffset.Now.AddMinutes(5.0);
_redisCache.SetCacheData<List<ProductDetails>>("product", productList, expirationTime);
return Ok(productList);
}
else
{
return NoContent();
}
}
/// <summary>
/// Get Product By Id
/// </summary>
/// <param name="productId"></param>
/// <returns></returns>
[HttpGet("{productId}")]
public async Task<ActionResult<ProductDetails>> GetProductDetailsByIdAsync(int productId)
{
var cacheData = _redisCache.GetCacheData<List<ProductDetails>>("product");
if (cacheData != null)
{
ProductDetails filteredData = cacheData.Where(x => x.Id == productId).FirstOrDefault();
return new ActionResult<ProductDetails>(filteredData);
}
var productDetails = await _productService.GetProductDetailByIdAsync(productId);
if(productDetails != null)
{
return Ok(productDetails);
}
else
{
return NotFound();
}
}
/// <summary>
/// Add a new product
/// </summary>
/// <param name="productDetails"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> AddProductAsync(ProductDetails productDetails)
{
var isProductInserted = await _productService.AddProductAsync(productDetails);
_redisCache.RemoveData("product");
if (isProductInserted)
{
return Ok(isProductInserted);
}
else
{
return BadRequest();
}
}
/// <summary>
/// Update product details
/// </summary>
/// <param name="productDetails"></param>
/// <returns></returns>
[HttpPut]
public async Task<IActionResult> UpdateProductAsync(ProductDetails productDetails)
{
var isProductUpdated = await _productService.UpdateProductAsync(productDetails);
_redisCache.RemoveData("product");
if (isProductUpdated)
{
return Ok(isProductUpdated);
}
else
{
return BadRequest();
}
}
/// <summary>
/// Delete product by id
/// </summary>
/// <param name="productId"></param>
/// <returns></returns>
[HttpDelete]
public async Task<IActionResult> DeleteProductAsync(int productId)
{
var isProductDeleted = await _productService.DeleteProductAsync(productId);
_redisCache.RemoveData("product");
if (isProductDeleted)
{
return Ok(isProductDeleted);
}
else
{
return BadRequest();
}
}
}
}
Step 15
Register a few services inside the Program class.
using AzureRedisCacheDemo.Data;
using AzureRedisCacheDemo.Models;
using AzureRedisCacheDemo.Repositories;
using AzureRedisCacheDemo.Repositories.AzureRedisCache;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using System;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddScoped < IProductService, ProductService > ();
builder.Services.AddDbContext < DbContextClass > (o => o.UseInMemoryDatabase("RedisCacheDemo"));
builder.Services.AddScoped < IRedisCache, RedisCache > ();
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
using(var scope = app.Services.CreateScope()) {
var services = scope.ServiceProvider;
var context = services.GetRequiredService < DbContextClass > ();
SeedData.Initialize(services);
}
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) {
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Step 16
Finally, run the application and we can see the swagger UI with our API’s endpoints.
Step 17
Hit the get the product endpoint and open the Redis CLI inside Azure Portal and there you can see the product list will get stored when we hit the endpoint the first time.
In this case, first, we check whether the data is present in a cache or not. If not then we get the data from the database and also save it into the cache. We already write the code related to that inside the controller. So, next time we get the data from the cache. If you put a debugger inside the controller then you can understand easily like how things are going to work.
GitHub URL
https://github.com/Jaydeep-007/AzureRedisCacheDemo/tree/master/AzureRedisCacheDemo
Conclusion
Here we looked into the cache introduction and their configuration on azure. Also, the step-by-step implementation using .NET Core Web API.