Implement In-Memory Cache in the NET Core API

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. This is because there are a lot of users using the internet. So 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. So because of that, one of the solutions is caching, and that’s why caching comes into the picture.

What Is Caching?

The cache is the memory storage that is 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.

Without Caching vs Using Chache

As you see in the above image, there are two scenarios: one without using cache and another with cache and how it works. So here, when we do not use the cache, in that case, suppose users want data. They will hit each time database, increasing the time complexity and reducing performance in case there is some static data users want. It is the same for all users; in that case, when we do not use the cache, then each one hits the unnecessary database to fetch data on the other side. As you can see, we use cache. In that case, if there is the same static and same data for all users, then only the first user will hit the database, fetch data, and store it in the cache memory. Then the other two users will use that from the cache without unnecessarily hitting database to fetch data.

Types of Cache

Basically, there are two types of caching .NET Core supports:

  1. In-Memory Caching
  2. 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. And in Distributed Caching, there are many third-party mechanisms like REDIS and many others. But in this section, we look into the In-Memory Cache in detail and how it works in the .NET Core.

In-Memory Cache

Basically, In-Memory Cache is used for lightweight and small applications, and that will work well in that. It stores data in the server memory on the application side, and users use that whenever they need that.

Advantages of In-Memory Cache

  • Users fetch data rapidly when we use In-Memory Cache.
  • It will increase the performance of the application.
  • Best suited for small application which is deployed on a single server.

Disadvantages of In-Memory Cache

  • Sometimes In-Memory cache increases the maintenance of the application
  • In the In-Memory Cache, data is persisted on a single server, and if the server crashes, then data is also lost; it’s hard to scale the application in some scenarios.

Now we will create one .NET Core API, Implement the caching into that, and understand how things will work.

Step 1

Create the .NET Core API Web Application.

Step 2

Install the following NuGet Packages, which need step by steps in our application.

Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Design
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
Swashbuckle.AspNetCore
System.Runtime.Caching

Step 3

Create the Model folder and create one Product Class inside that with details.

namespace MemoryCacheDemo.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 have shown below.

using MemoryCacheDemo.Model;
using Microsoft.EntityFrameworkCore;
namespace MemoryCacheDemo.Data
{
    public class DbContextClass: DbContext
    {
        public DbContextClass(DbContextOptions<DbContextClass> options) : base(options)
        {
        }
        public DbSet<Product> Products { get; set; }
    }
}

Step 5

Now, we will create ICacheService Interface and CacheService Class for In-Memory Cache-related usage.

using System;
using System.Collections.Generic;
namespace MemoryCacheDemo.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 a Cache Service.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Caching;
namespace MemoryCacheDemo.Cache
{
    public class CacheService : ICacheService
    {
        ObjectCache _memoryCache = MemoryCache.Default;
        public T GetData<T>(string key)
        {
            try
            {
                T item = (T)_memoryCache.Get(key);
                return item;
            }
            catch (Exception e)
            {
                throw;
            }
        }
        public bool SetData<T>(string key, T value, DateTimeOffset expirationTime)
        {
            bool res = true;
            try
            {
                if (!string.IsNullOrEmpty(key))
                {
                    _memoryCache.Set(key, value, expirationTime);
                }
            }
            catch (Exception e)
            {
                throw;
            }
            return res;
        }
        public object RemoveData(string key)
        {
            try
            {
                if (!string.IsNullOrEmpty(key))
                {
                    return _memoryCache.Remove(key);
                }
            }
           catch(Exception e)
            {
                throw;
            }
            return false;
        }
    }
}

Step 6

Create the ProductController class and create the following method, as shown below.

using MemoryCacheDemo.Cache;
using MemoryCacheDemo.Data;
using MemoryCacheDemo.Model;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace MemoryCacheDemo.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 inside appsetting.json.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=server;Initial Catalog=MemoryCache;User Id=****;Password=***;"
  }
}

Step 8

Next, Register the ICacheService inside Configure Service method of the Startup Class and also add some configuration related to Swagger to test our API endpoints.

using MemoryCacheDemo.Cache;
using MemoryCacheDemo.Data;
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;
namespace MemoryCacheDemo
{
    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 = "MemoryCacheDemo", 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", "MemoryCacheDemo v1"));
            }
            app.UseHttpsRedirection();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

Step 9

Perform Migration and Database Updates for DB Creation using the following commands in Package Manager Console.

add-migration “FirstMigration”update-database

So, when you enter and execute this command it will generate a few things related to migration and create the database inside SQL Server as you put inside Connection String in the appsetting.json.

Step 10

Finally, run the application and add the data using swagger UI and then check how caching works inside products and product endpoint.

Basically, I added cache into the product and products endpoints in the controller; as you see, when the user wants to fetch data of all products, then firstly, it will check whether the data is present inside the In-Memory Cache or not and if it’s present inside the cache then return that data to the user and if the data is not present inside the cache, then it will fetch the data from database and also, set that into the cache. So next time user will get that from the cache only and avoid hitting the database unnecessarily.

Also, 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 to the user from cache and not then fetch from database and return to the user after applying the filter.

So, as you see inside the update, delete and post endpoint of the Product Controller, then 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 use of memory caches you can use as per your need and requirements. I just want to introduce the basics of the Memory Cache and How it works inside the .NET Core that I covered here.

Also, there is one scenario you need to take care of while using caching. Suppose two users are using your application; then the following scenarios will come:

  • When the First User sends the request to fetch the data of all products, then the first request comes, and then it 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.
  • Meanwhile, the Second user sends the request to get the product details. Then what happened is the request also hits the database before completing the first user’s request, and because of that second user also hits the database to fetch product details.
  • So, one solution for this is to use the Lock Mechanism, as shown below.

Add create a lock object top of the class and then in the method as I showed below.

private static object _lock = new object();

Next, add the lock in Get Method.

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;
}

So here, as you see first, we check whether the data is present inside the cache. If data is available, then return that. Next, if the value is not present in the memory cache, we apply the lock over there. Then the request is locked and entered into the section, fetches the product details from the database, then set to the cache, and then returns the data. So, what happens when the second user sends a request before the user one request is complete? So, in that case, the second request is in the queue, and after completing the first user request the second request comes into the picture.

This is all about In-Memory Cache in .NET Core. I hope you understand things related to that.

Happy Coding!


Source link