Redis Caching with IDistributedCache in C

This article provides a practical guide to implementing distributed caching in .NET applications using the built-in IDistributedCache interface backed by Redis. You will learn which NuGet packages to install, how to register Redis within the dependency injection container, and how to write efficient, reusable methods for storing, retrieving, and serializing cached data using the cache-aside pattern.

1. Install the Required NuGet Package

To enable Redis support for IDistributedCache, install the Microsoft.Extensions.Caching.StackExchangeRedis package via the .NET CLI or the NuGet Package Manager:

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis

2. Configure Redis in Application Startup

Open Program.cs and register the Redis cache service with the dependency injection container using AddStackExchangeRedisCache.

var builder = WebApplication.CreateBuilder(args);

// Register Redis Distributed Cache
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis") ?? "localhost:6379";
    options.InstanceName = "SampleApp_"; // Prefix for all keys stored in Redis
});

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

In appsettings.json, add the connection string:

{
  "ConnectionStrings": {
    "Redis": "localhost:6379,abortConnect=false"
  }
}

3. Create Extension Methods for Serialization

IDistributedCache natively works only with byte[] arrays or strings. To handle complex C# objects seamlessly, create helper extension methods using System.Text.Json:

using System.Text.Json;
using Microsoft.Extensions.Caching.Distributed;

public static class DistributedCacheExtensions
{
    public static async Task SetRecordAsync<T>(
        this IDistributedCache cache,
        string recordId,
        T data,
        TimeSpan? absoluteExpireTime = null,
        TimeSpan? slidingExpireTime = null)
    {
        var options = new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = absoluteExpireTime ?? TimeSpan.FromMinutes(60),
            SlidingExpiration = slidingExpireTime
        };

        var jsonData = JsonSerializer.Serialize(data);
        await cache.SetStringAsync(recordId, jsonData, options);
    }

    public static async Task<T?> GetRecordAsync<T>(
        this IDistributedCache cache,
        string recordId)
    {
        var jsonData = await cache.GetStringAsync(recordId);

        if (string.IsNullOrEmpty(jsonData))
        {
            return default;
        }

        return JsonSerializer.Deserialize<T>(jsonData);
    }
}

4. Implement the Cache-Aside Pattern

Inject IDistributedCache into your services or controllers. When requesting data, check the cache first. If the item is absent (a cache miss), retrieve it from the database, save it into Redis, and return it.

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

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IDistributedCache _cache;

    public ProductsController(IDistributedCache cache)
    {
        _cache = cache;
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetProduct(int id)
    {
        string cacheKey = $"Product_{id}";

        // Attempt to fetch from Redis
        var product = await _cache.GetRecordAsync<Product>(cacheKey);

        if (product is null)
        {
            // Cache miss: retrieve from database
            product = await FetchProductFromDatabaseAsync(id);

            if (product is null)
            {
                return NotFound();
            }

            // Save to Redis with a 5-minute absolute expiration and 1-minute sliding expiration
            await _cache.SetRecordAsync(
                cacheKey, 
                product, 
                absoluteExpireTime: TimeSpan.FromMinutes(5), 
                slidingExpireTime: TimeSpan.FromMinutes(1));
        }

        return Ok(product);
    }

    private static async Task<Product?> FetchProductFromDatabaseAsync(int id)
    {
        // Simulated database call
        await Task.Delay(100);
        return new Product { Id = id, Name = "Mechanical Keyboard", Price = 99.99m };
    }
}

public record Product
{
    public int Id { get; init; }
    public string Name { get; init; } = string.Empty;
    public decimal Price { get; init; }
}

5. Invalidate or Remove Cached Items

When an entity is updated or deleted in the database, clear the corresponding cache key to prevent stale data:

[HttpDelete("{id}")]
public async Task<IActionResult> DeleteProduct(int id)
{
    // Perform database deletion logic here...

    // Invalidate the cache
    string cacheKey = $"Product_{id}";
    await _cache.RemoveAsync(cacheKey);

    return NoContent();
}