Implementation of the REDIS Cache in the .NET Core API
In this article, readers will understand what caching is, the types of caches, how to install REDIS cache, and how to implement REDIS cache using .NET Core API.
Join the DZone community and get the full member experience.
Join For FreeIn this article, I cover caching and how it works in .NET Core. So, we look at the following things one by one:
- Introduction of Caching
- What is Cache?
- Types of Cache
- Installation of REDIS Cache
- Cache Implementation
So, let’s get started!
Introduction
Caching is very popular nowadays in the software industry because it will improve the performance and scalability of the application, as we see with many web applications like e-mail and Facebook, and how responsive they are and how great the user experience is while using them. 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 that help us improve the performance and responsiveness of the application. These things need solutions, which is where caching enters the picture.
What Is Caching?
The cache is the memory storage used to store the frequent access data in the temporary storage; it will improve the performance drastically, avoid unnecessary database hits, and store frequently used data into the buffer whenever we need it.
As you see in the above image, there are two scenarios:
- Without using cache.
- Using cache and seeing how it works.
So, if we do not use the cache, if users want data, they will hit the database each time, which will increase the time complexity and reduce performance if there is some static data users want, and it is the same for all users. On the other hand, as you can see, we use the cache. In that case, if there is the same static and same datum, each user will hit the unnecessary database to fetch data.
Types of Cache
There are two types of caching .NET Core supports:
- In-memory caching
- Distributed caching
When we use in-memory caching, data is stored in the application server memory, and whenever we need it, we fetch it from there and use it wherever we need it. There are also many third-party mechanisms for distributed caching, such as REDIS and others. But in this section, we look into the REDIS cache in detail and how it works in the .NET Core.
Distributed Caching
- 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 restarted, 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 usage one by one.
REDIS cache:
- REDIS is an open source (BSD Licensed) in-memory data structure store used as a database.
- 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, which we can use, like list, set, hashing, stream, and many more, to store the data.
Installation of REDIS Cache
Step 1
Download the REDIS server using the following URL.
Step 2
Extract the zip file and, later on, open the REDIS server and REDIS CLI:
Implementation of REDIS Cache Using .NET Core API
Step 1
Create the .NET Core API web application.
Step 2
Install the following “NuGet Packages,” which need step by step in our application:
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Design
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
Swashbuckle.AspNetCore
StackExchange.Redis
Step 3
Create the “Model” folder and create one product class inside that with the following details:
namespace RedisCacheDemo.Model
{
public class Product
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public string ProductDescription { get; set; }
public int Stock { get; set; }
}
}
Step 4
Next, create the DbContextClass
class for database-related operations as I show below:
using Microsoft.EntityFrameworkCore;
using RedisCacheDemo.Model;
namespace RedisCacheDemo.Data
{
public class DbContextClass: DbContext
{
public DbContextClass(DbContextOptions<DbContextClass> options) : base(options)
{
}
public DbSet<Product> Products { get; set; }
}
}
Step 5
Now, we are going to create an ICacheService
interface and CacheService
class for the REDIS cache-related usage:
using System;
namespace RedisCacheDemo.Cache
{
public interface ICacheService
{
/// <summary>
/// Get Data using key
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <returns></returns>
T GetData<T>(string key);
/// <summary>
/// Set Data with Value and Expiration Time of Key
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="expirationTime"></param>
/// <returns></returns>
bool SetData<T>(string key, T value, DateTimeOffset expirationTime);
/// <summary>
/// Remove Data
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
object RemoveData(string key);
}
}
Next, create the CacheService
class:
using Newtonsoft.Json;
using StackExchange.Redis;
using System;
namespace RedisCacheDemo.Cache
{
public class CacheService : ICacheService
{
private IDatabase _db;
public CacheService()
{
ConfigureRedis();
}
private void ConfigureRedis()
{
_db = ConnectionHelper.Connection.GetDatabase();
}
public T GetData<T>(string key)
{
var value = _db.StringGet(key);
if (!string.IsNullOrEmpty(value))
{
return JsonConvert.DeserializeObject<T>(value);
}
return default;
}
public bool SetData<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;
}
public object RemoveData(string key)
{
bool _isKeyExist = _db.KeyExists(key);
if (_isKeyExist == true)
{
return _db.KeyDelete(key);
}
return false;
}
}
}
Step 6
Create the ProductController
class. Create the following method as shown below:
using Microsoft.AspNetCore.Mvc;
using RedisCacheDemo.Cache;
using RedisCacheDemo.Data;
using RedisCacheDemo.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace RedisCacheDemo.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ProductController : ControllerBase
{
private readonly DbContextClass _dbContext;
private readonly ICacheService _cacheService;
public ProductController(DbContextClass dbContext, ICacheService cacheService)
{
_dbContext = dbContext;
_cacheService = cacheService;
}
[HttpGet("products")]
public IEnumerable<Product> Get()
{
var cacheData = _cacheService.GetData<IEnumerable<Product>>("product");
if (cacheData != null)
{
return cacheData;
}
var expirationTime = DateTimeOffset.Now.AddMinutes(5.0);
cacheData = _dbContext.Products.ToList();
_cacheService.SetData<IEnumerable<Product>>("product", cacheData, expirationTime);
return cacheData;
}
[HttpGet("product")]
public Product Get(int id)
{
Product filteredData;
var cacheData = _cacheService.GetData<IEnumerable<Product>>("product");
if (cacheData != null)
{
filteredData = cacheData.Where(x => x.ProductId == id).FirstOrDefault();
return filteredData;
}
filteredData = _dbContext.Products.Where(x => x.ProductId == id).FirstOrDefault();
return filteredData;
}
[HttpPost("addproduct")]
public async Task<Product> Post(Product value)
{
var obj = await _dbContext.Products.AddAsync(value);
_cacheService.RemoveData("product");
_dbContext.SaveChanges();
return obj.Entity;
}
[HttpPut("updateproduct")]
public void Put(Product product)
{
_dbContext.Products.Update(product);
_cacheService.RemoveData("product");
_dbContext.SaveChanges();
}
[HttpDelete("deleteproduct")]
public void Delete(int Id)
{
var filteredData = _dbContext.Products.Where(x => x.ProductId == Id).FirstOrDefault();
_dbContext.Remove(filteredData);
_cacheService.RemoveData("product");
_dbContext.SaveChanges();
}
}
}
Step 7
Add the SQL server connection string and the REDIS URL inside the appsetting.json
:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"RedisURL": "127.0.0.1:6379",
"ConnectionStrings": {
"DefaultConnection": "Data Source=Server;Initial Catalog=RedisCache;User Id=sa;Password=***;"
}
}
Step 8
Next, register the ICacheService
inside the configure service method of the startup class and add some configuration related to Swagger to test our API endpoints:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using RedisCacheDemo.Cache;
using RedisCacheDemo.Data;
namespace RedisCacheDemo
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddScoped<ICacheService, CacheService>();
services.AddDbContext<DbContextClass>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "RedisCacheDemo", Version = "v1" });
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "RedisCacheDemo v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
Step 9
Create one ConfigurationManger
class to configure the app setting over there:
using Microsoft.Extensions.Configuration;
using System.IO;
namespace RedisCacheDemo
{
static class ConfigurationManager
{
public static IConfiguration AppSetting { get; }
static ConfigurationManager()
{
AppSetting = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
}
}
}
Step 10
Next, create the connection helper class for the REDIS connection:
using StackExchange.Redis;
using System;
namespace RedisCacheDemo.Cache
{
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 11
Perform a migration and database update for the DB creation using the following commands in the package manager console:
add-migration “FirstMigration”update-database
When you enter and execute this command, it will generate a few things related to migration and create the database inside the SQL server as you put it inside the connection string in the appsetting.json
.
Step 12
Finally, run the application and add the data using Swagger UI and check to see how caching works inside the products and product endpoint.
I added a cache into the product and products endpoints in the controller, as you see when the user wants to fetch data of all products. Next, it will check whether the data is present inside the REDIS cache or not and, if it’s present inside the cache, then return that data to the user. If the data is not present inside the cache, it will fetch the data from the database and set that into the cache. Next time, the user will get that from the cache only and avoid hitting the database unnecessarily.
When the user wants to fetch data by using the product id, as you see in the controller in the product second endpoint, we fetch data from the cache of all products, then filter using the product id, and, if that will present, then return the user from the cache. If not, fetch it from the database and return it to the user after applying the filter.
As you see inside the update, delete, and post endpoint of the product controller, we use the remove
method to remove the data of the product key, which is present inside the cache. So, there are many scenarios and uses of memory caches you can use as per your needs and requirements. My main goal is to introduce the basics of the REDIS cache and how it works inside the .NET Core I covered here.
There is one scenario you need to take care of while using caching. Suppose two users are using your application; the following scenarios will come:
- When the first user sends the request to fetch the data of all products, the first request comes and will check if the data is present inside the cache or not. If the data is present inside the cache, it will fetch the data from the database and set it to the cache.
- The second user sends the request to get the product details. Next, what happens is the request hits the database before completing the first user’s request, and because of that, the second user hits the database to fetch the product details.
- So, there is one solution for this: to use the “Lock Mechanism” as shown below:
Create this private object of the lock on top of the class:
private static object _lock = new object()
Next, modify the Get
method as shown below:
public IEnumerable<Product> Get()
{
var cacheData = _cacheService.GetData<IEnumerable<Product>>("product");
if (cacheData != null)
{
return cacheData;
} lock (_lock)
{
var expirationTime = DateTimeOffset.Now.AddMinutes(5.0);
cacheData = _dbContext.Products.ToList();
_cacheService.SetData<IEnumerable<Product>>("product", cacheData, expirationTime);
} return cacheData;
}
Here, we check to see if the data is present inside the cache or not. If the data is available, return that. Next, if the value is not present in the REDIS cache, we apply the lock over there, and the request is locked and entered into the section and fetches the product details from the database. After, it sets it to the cache and returns the data.
What happened when the second user sends a request before the first user’s request is complete? In that case, the second request is in the queue, and after completing the first user request, the second request comes into the picture.
Also, you can see key details, which already present inside REDIS using REDIS CLI as shown below:
Here you can see many commands that provide us with information about the keys that are present in the REDIS cache.
If you’re interested, here is my GitHub link.
Conclusion
This article was all about the REDIS cache in .NET Core. By now, you should understand what caching is, the types of caches, how to install REDIS cache, and how to implement REDIS cache using .NET Core API.
Happy Coding!
Published at DZone with permission of Jaydeep Patil. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments