Implement Hybrid Cache in .NET 9 + Redis Caching

Hybrid cache

The hybrid cache is a unified library for caching data in-memory and external sources, which can also be referred to as multi-tier caching. In other words, this mult-tier caching is a replacement for IDistributedCache and IMemoryCache. It is also intended to simplify the usage of caching in .NET. The older approach for Distributed Caching required writing additional code to ensure things are cached and retrieved properly. This package is an amazing addition to simplifying caching in .NET to make a robust caching solution. We use Microsoft to implement a hybrid cache.Extensions.Caching.Hybrid nuget package. In this article, we will cover all the features of the Hybrid cache package.

Package Features

  • Extensible code: Code written for in-memory cache can be used as-is without modification for integration with external caching servers like Redis, SQL Server, etc.
  • Concurrency Management: To use the hybrid cache, we have to use the Hybrid cache class as a dependency service. This class ensures that only instance is used to get data for any key and no concurrent requests are made for the same entry. All the concurrent requests wait for this request to get completed.
  • Multi-source caching/Primary-secondary source caching: If the application has configured multiple sources of data for caching, data is stored at all locations.

To get data, it is first checked in primary source. If that data is not available, the request is forwarded to a secondary source, as shown in the diagram below, scenario 1 in blue and scenario 2 in red.

Diagram

Code setup

To get started, we will first install Microsoft.Extensions.Caching.Hybrid package.

In Program.cs, we will write the below code to add the Hybrid cache.

builder.Services.AddHybridCache();

To configure the hybrid cache, we write the below code inside AddHybridCache as a setup action. Below is an example.

  builder.Services.AddHybridCache(options =>
  {
      options.ReportTagMetrics = true;
      options.DefaultEntryOptions = new Microsoft.Extensions.Caching.Hybrid.HybridCacheEntryOptions
      {
          Expiration = TimeSpan.FromSeconds(30),
          LocalCacheExpiration = TimeSpan.FromSeconds(30),
          
      };
  });

Implement In-memory cache

The above depenedency service configuration is sufficient for us to perform in-memory caching. Below is the sample code in the API controller to set and get the key and value in application memory.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Hybrid;

namespace HybridCacheDemoApplication.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class CacheController : ControllerBase
    {
        private readonly HybridCache _cache;
        const string cacheKey = "ping";
        public CacheController(HybridCache cache)
        {
            _cache = cache;
        }

        [HttpGet(Name = "GetPingKey")]
        public async Task<IActionResult> Get()
        {
            var keydata = await _cache.GetOrCreateAsync<string>(
                cacheKey, // Unique key to the cache entry
                async cancel => await Task.FromResult("pong from get")
            );
            return Ok(new {message = $"{cacheKey} data: {keydata}"});
        }

        [HttpGet("set",Name = "SetPingKey")]
        public async Task<IActionResult> Set()
        {
            await _cache.SetAsync<string>(cacheKey, "ping");
            return Ok(new { message = $"{cacheKey} is set" });
        }
    }
}

To understand how this code works, see the below table in sequence.

Cache execution demo table
 

Sequence Endpoint Output Explanation
1 http://localhost:5154/cache/
{
  "message": "ping data: pong from get"
}
This endpoint sets the cache value as "pong from get" when the value is not set; hence, it will print "pong from get"
2 http://localhost:5154/cache/set
{
  "message": "ping is set"
}
This endpoint will set the value in the cache.
3 http://localhost:5154/cache/
{
  "message": "ping data: ping"
}
This is the same endpoint we invoked in 1 step, but this time, the value already exists in the cache.

Delete Cache Key: To remove cache keys, we use the below implementation,

[HttpGet("del",Name="DeletePingKey")]
public async Task<IActionResult> RemoveKey()
{
    await _cache.RemoveAsync(cacheKey); //Removes data
    return Ok(new {message=$"{cacheKey} removed"});
}

Distributed Caching using Redis Server

To implement distributed caching, we need to perform additional dependency service configurations. For this example, we will use Redis cache as an external caching source.

Now, we will configure the Redis package using Microsoft.Extensions.Caching.StackExchangeRedis

Once installed, we need to add the below code to add Redis. Our connection string will be in format {HOST_NAME}:{PORT_NUMBER},password={PASSWORD}

   builder.Services.AddStackExchangeRedisCache(options =>
   {
       options.Configuration =
           builder.Configuration.GetConnectionString("RedisConnectionString");
   });

Now, we will invoke http://localhost:5154/cache/set, and data will be set in the Redis cache seamlessly. Below is how it looks in Redis

Database

Notice how seamlessly our code worked just by adding a dependency for a distributed caching server, i.e., Redis, in our scenario.

Serialization

To send data to an external caching server, this package uses byte[], string, and System.Text.Json. This is further customizable using .AddSerializer() or AddSerializerFactory() methods additionally with AddHybridCache dependency service. When we use a more optimized serializer, they improve the performance of the application.

More customizations

We can further customize the behavior of cache services by setting flags when configuring dependencies. Below is one sample flag to disable compression for caching.

Note. This is not a best practice, but it may vary based on the scenario when compression is not needed or used.

 builder.Services.AddHybridCache(options =>
 {
     options.ReportTagMetrics = true;
     options.DefaultEntryOptions = new Microsoft.Extensions.Caching.Hybrid.HybridCacheEntryOptions
     {
         Flags = Microsoft.Extensions.Caching.Hybrid.HybridCacheEntryFlags.DisableCompression

     };
 });

Experimental feature: Tags - Working with inter-related keys

At times, we may be dealing with many& interrelated keys. Example: In an e-commerce application, we have cached a lot of fields that correspond to a cart item to a user. For this, we can create tags.

Below is the code, for working with inter-related keys we have created tag named "testdata", we can add more than one tag depending upon type of grouping applicable for different scenarios. This code is self-explanatory, but in case of any confusion, mention it in the comments.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Hybrid;

namespace HybridCacheDemoApplication.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class CacheController : ControllerBase
    {
        private readonly HybridCache _cache;
        const string cacheKey = "ping";
        public CacheController(HybridCache cache)
        {
            _cache = cache;
        }

        [HttpGet(Name = "GetPingKey")]
        public async Task<IActionResult> Get()
        {
            var keydata = await _cache.GetOrCreateAsync<string>(
                cacheKey, // Unique key to the cache entry
                async cancel => await Task.FromResult("pong from get"), 
                tags: ["testdata"]
            );
            return Ok(new {message = $"{cacheKey} data: {keydata}"});
        }

        [HttpGet("set",Name = "SetPingKey")]
        public async Task<IActionResult> Set()
        {
            await _cache.SetAsync<string>(cacheKey, 
                "ping", 
                tags: ["testdata"]
            );
            return Ok(new { message = $"{cacheKey} is set" });
        }

        [HttpGet("del",Name="DeletePingKey")]
        public async Task<IActionResult> RemoveKey()
        {
            await _cache.RemoveAsync(cacheKey);
            return Ok(new {message=$"{cacheKey} removed"});
        }

        [HttpGet("delbytag", Name = "DeleteByTestTags")]
        public async Task<IActionResult> RemoveKeyByTags()
        {
            await _cache.RemoveByTagAsync(["testdata"]);
            return Ok(new { message = $"{cacheKey} removed" });
        }
    }
}

Thanks for reading till the end. This Hybrid caching offers amazing benefits, and I hope you, too, will find this helpful.