Implement a distributed Redis cache in ASP.Net Core Web API


Redis Caching in ASP.NET Core



What is a distributed Cache?


 A cache shared by multiple app servers is typically maintained as an external service to the app servers that access it. 

A distributed cache can improve the performance and scalability of an ASP.NET Core app, especially when the app is hosted by a cloud service or a server farm (multi-server environment). 

A distributed cache has several advantages over other caching scenarios where cached data is stored on individual app servers.

A distributed cache is shared across multiple application servers. The cached data doesn’t reside in the memory of an individual web server. But this data is stored centrally so it is available to all of the application’s servers.

If some of the servers down or stop responding due to technical or network issues, other servers will still be able to retrieve the cached data. And one more advantage of a distributed cache is that the cached data is still available even the server restarts. Since cache works independently will not affect when you can still be able to add or remove a server.  

These are the 4 types of distributed caches, and this article covers Redis Distributed Cache in step-by-step implementation.

In ASP.NET Core using Redis distributed cache is that simple to do when you through the entire article.  The Distributed caching features implemented in ASP.NET Core using C# program. This article covers ASP.NET Core Web API 2 with Azure Redis Cache implementation in an easy way.
  
  
  

Types of Distributed Cache
  • Distributed Memory Cache
  • Distributed SQL Server Cache
  • Distributed  Redis Cache
  • Distributed NCache Cache

 

What is Redis Cache?


Redis Caching in ASP.NET Core

 

Redis is an open-source in-memory data store, which is often used as a distributed cache. You can configure an Azure Redis Cache for an Azure-hosted ASP.NET Core app, and use an Azure Redis Cache for local development.

Azure Cache for Redis provides an in-memory data store based on the Redis software. Redis improves the performance and scalability of an application that uses backend data stores heavily. It can process large volumes of application requests by keeping frequently accessed data in the server memory that can be written to and read quickly. Redis brings a critical low-latency and high-throughput data storage solution to modern applications. 


How do I create Azure Redis Cache?


To continue the next section I assume you have created Azure Redis Cache Database. 

If you are new to Azure Redis  How to create an Azure Redis Distributed Cache

* Please note to secure when you copy Azure Primary Access Key 



What tools are used to implement?


Create a new project in Visual Studio 2017 or Visual Studio 2019, choose the ASP.NET Core Web API 2 project with an empty template. 

If you are new to this how to create ASP.NET Core Web API 2 - here it is

I created a new Web API project- what is next?

You need to know a bit about IDistributedCache Interface
 


What is IDistributedCache?


The IDistributedCache interface includes synchronous and asynchronous methods. The interface allows items to be added, retrieved, and removed from the distributed cache implementation. 

Using this interface is an incorporate a method to implement in your services because this interface is common for any distributed cache you choose.

The IDistributedCache interface includes the following methods:

Get, GetAsync
Takes a string key and retrieves a cached item as a byte[] if found in the cache.

Set, SetAsync
Adds an item (as byte[]) to the cache using a string key. You must save data in byte array content

Refresh, RefreshAsync
Refreshes an item in the cache based on its key, resetting its sliding expiration timeout (if any).

Remove, RemoveAsync
Removes a cache entry based on its key.

IDistributedCache

public interface IDistributedCache
   {
      byte[] Get(string key);
      Task<byte[]> GetAsync(string key, CancellationToken token = default(CancellationToken));
      void Refresh(string key);
      Task RefreshAsync(string key, CancellationToken token = default(CancellationToken));
      void Remove(string key);
      Task RemoveAsync(string key, CancellationToken token = default(CancellationToken));
      void Set(string key, byte[] value, DistributedCacheEntryOptions options);
      Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default(CancellationToken));
   }


Step 1: How to begin with Visual Studio 2017 or 2019?


First Step 

Go to your Visual Studio new project solution created, and add dependency supporter for Azure Redis Distributed Cache "Microsoft.Extensions.Caching.Redis

1. Right-click on the Solution Explorer,
2. Choose "Manage NuGet packages" 
3. Search for  "Microsoft.Extensions.Caching.Redis"  and install



Microsoft.Extensions.Caching.Redis



*Note: I installed version 2.1.2 because of Visual Studio 2017 project and a .NET Core 2.1

Step 2:  Add Azure Redis Connection String?


Go to your Azure portal http://portal.azure.com/  to copy the Primary Connection String as shown below

azure redis cache connection string


Open the project add the following code in the appsettings.json file, the connection string is added under "RedisCache" section

add connection string in appsettings.json



Step 3:  How to read Connection String in Startup class?


Open the project and click on the "Startup. cs" file, 

Go to a method  public void ConfigureServices(IServiceCollection services)

Add these lines of code inside 

public void ConfigureServices(IServiceCollection services)
{
    //Redis DB Connection
     services.AddDistributedRedisCache(option =>
            {
               option.Configuration = Configuration.GetSection("RedisCache").GetSection("ConnectionString").Value;
            });
}

 

Step 4:  How do I save and retrieve data from Azure Redis Cache?


The data is stored in Azure as a key value-based and content is stored as a byte [ ] array with a time interval of expiry.

For example SaveData(key, byte [])  Key param is a string type

Let's make use of the Dependency interface IDistributedCache in a class object. If you have read the above section of Step1 interface methods are explicitly displaced.

This is a simple class that explains as below,

Create an interface "IRedisCache" that implements three methods


IRedisCache.cs

 public interface IRedisService
   {
      T GetRedisData<T>(string key);
      string GetRedisData(string key);
      void SetRedisData(string key, byte[] value, int expiry);
   }  

Method 1 : T GetRedisData<T>(string key);

This is to retrieve by key and return a class object stored in Azure  

Method 2 : string GetRedisData(string key);

This is to retrieve by key and return a string stored in Azure  

Method 3 : void SetRedisData(string key, byte[] value, int expiry);

This is to store data or class object convert into an array byte by key and set with an expiry of a resource.

Now, create a class "RedisCache" that implements "IRedisCache". 

If you notice the below class that implements ILoggerService  East way to implement

This is the class we implement IDistributedCache  in constructor DI for accessing Redis Azure base 

Note
Consider RedisCache as a base class because when the main class DemoService.cs  (in the next section)  will make use of the IRedisCache methods implemented in RedisCache. This is to hide method implementation and to make it available as method declaration.

RedisCache.cs [base class]

  public class RedisService : IRedisService
   {
      private readonly ILoggerService _loggerService;
      private readonly IDistributedCache _cache;

      public RedisService(IDistributedCache cache, ILoggerService loggerService)
      {
         _cache = cache;
         _loggerService = loggerService;
      }

      public void SetRedisData(string key, byte[] value, int expiry)
      {
         try
         {
            if (key == null) throw new Exception($"Unable to define key={key}.");

            _cache.Set(key, value, new DistributedCacheEntryOptions()
            {
               AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(expiry)
            });
         }
         catch (Exception ex)
         {
            _loggerService.LogMessage(ex);
            throw ex;
         }
      }
      public T GetRedisData<T>(string key)
      {
         try
         {
            var decodedStr = GetRedisData(key);
            return JsonConvert.DeserializeObject<T>(decodedStr);
         }
         catch (Exception ex)
         {
            _loggerService.LogMessage(ex);
            throw ex;
         }
      }
      public string GetRedisData(string key)
      {
         try
         {
            var data = _cache.Get(key);
            if (data == null) throw new Exception($"Unable to find data for key={key}.");

            var decodedStr = Encoding.UTF8.GetString(data);
            if (string.IsNullOrEmpty(decodedStr)) throw new Exception($"Decoded data is empty for key={key}");

            return decodedStr;
         }
         catch (Exception ex)
         {
            _loggerService.LogMessage(ex);
            throw ex;
         }
      }
   }


So far we have defined our base class interface and class.

Next is to create a model  Product.cs that represents a class object or entry to be stored in Azure

Product. cs

  public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Category { get; set; }
        public decimal Price { get; set; }
    }


Now let's create an interface  IDemoService.cs  for method declaration.

IDemoService.cs

 public interface IDemoService
   {
      IActionResult FetchData(string userId);
      IActionResult FetchObjectData(string userId);
      IActionResult SaveData(string userId,Product product);
   }


On following create a service class  DemoService.cs that implements the IDemoService interface


DemoService. cs

using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Demo.Services
{
   public class DemoService : IDemoService
   {
      /// <summary>
      /// Declare Interface as private readonly for secure
      /// </summary>
      private readonly IRedisService _redisService;

      /// <summary>
      /// Construction interface object Dependency Injection
      /// </summary>
      /// <param name="redisService"></param>
      public DemoService(IRedisService redisService)
      {
         _redisService = redisService;
      }

      /// <summary>
      /// Save a product entry to Azure
      /// </summary>
      /// <param name="userId"></param>
      /// <returns>Ok Success</returns>
      public IActionResult SaveData(string userId, Product product)
      {
         //Save in Azure Redis Cache

         //1.Convert to json object
         //2.Convert to Bytes[]
         var _productdata = this.ToBytes(this.ToJson(product));

         //3.Save with unique key,data and expiry time - ** Note to remeber Key to retrive
         _redisService.SetRedisData(userId, _productdata, 30);
         return JsonOk("Successfully Saved");
      }

      /// <summary>
      /// Fetch class object from Azure Redis
      /// </summary>
      /// <param name="userId"></param>
      /// <returns>Product entry</returns>
      public IActionResult FetchObjectData(string userId)
      {
         var result = _redisService.GetRedisData<Product>(userId);
          
         return JsonOk(result);
      }

      /// <summary>
      /// Fetch string value from Azure Redis
      /// </summary>
      /// <param name="userId"></param>
      /// <returns>string</returns>
      public IActionResult FetchData(string userId)
      {
         var result = _redisService.GetRedisData(userId);

         return JsonOk(result);
      }

      private string ToJson(object obj)
      {
         return JsonConvert.SerializeObject(obj);
      }
      private byte[] ToBytes(string jsonText)
      {
         if (string.IsNullOrEmpty(jsonText)) throw new Exception($"The json text is empty = {jsonText}");
         return Encoding.UTF8.GetBytes(jsonText);
      }
      private OkObjectResult JsonOk(object obj)
      {
         return new OkObjectResult(JsonConvert.SerializeObject(obj));
      }
   }
}

So now you have successfully created a Demo service class implemented the base class. 

Create a Web API Controller

The next final action is to create a  Controller  DemoController.cs implement the demo services, 
this controller created to execute by the below functions.

1. Save Product Data [POST] 
2. Retrieve Product Data as class object entry [GET] 
3. Retrieve Product property as a string value [GET] 

I used the controller action method parameter as [Frombody] to get input from user request

DemoController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using IRAS_API_POC.Interfaces;
using IRAS_API_POC.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace IRAS_API_POC.Controllers.abc
{
  
   public class DemoController : ControllerBase
    {

      private readonly IDemoService _demoService;

      public DemoController(IDemoService demoService)
      {
         _demoService = demoService;
      }

      [HttpPost]
      public IActionResult SaveProduct([FromBody] string userId, Product product)
      {
         if (product == null)
         {
            new Product
            {
               Id = 1,
               Category = "A",
               Name = "Dell",
               Price = 34
            };
         }
         return _demoService.SaveData(userId, product);
      }

      [HttpGet]
      public IActionResult GetProduct([FromBody] string userId)
      {
         //returns product object data
         return _demoService.FetchObjectData(userId);
      }

      [HttpGet]
      public IActionResult GetProductData([FromBody] string userId)
      {
         //returns string value
         return _demoService.FetchData(userId);
      } 
   }
}


The code ends with this web API controller. When you execute each method in the Postman tool would get a result. 


Summary


Until now, we have learned basic concepts of Distributed Cache and types, in this article covered Azure Redis Distributed cache and its implementation in Web API step-by-step.


What are the tools used?

Development Tool: Visual Studio 2017

Project Temple Framework:  ASP.NET Core Web API 2

Database: Azure Redis Distributed Cache

We identified the IDistrubuted Cache interface that has saving, retrieval methods for azure cache implementation.

Overall What we implemented?

1. Created interface IRedisCache, which is inherited in RedisCache base class. 

2. Created a middle class or layer interface IDemoService, which is inherited in DemoService class

3. Created a Controller which is DemoController  and used IDemoService interface as Constructor Dependency Injection 

What actually we did with the Azure Redis Distributed Cache?

1. Created an Azure Redis distributed cache in Azure Portal 

2. Initiated Redis Connection String in appSettings.json

3. Created a Product Model class with properties

4. Created base class interfaces and classes

5. Created middle layer service that will Save Data, Retrieve Data string, Retrieve class object.

6. Created a Controller which executes all three actions methods based on user request send.