How to implement the Cache-Aside pattern using Redis in C# .NET?

Asked 19 hours ago Updated 18 hours ago 29 views

1

The Cache-Aside pattern (also known as lazy loading) ensures application code reads data from cache first. If a cache miss occurs, data is retrieved from the primary database and written back into Redis for future requests.

C# .NET Implementation Example

Here is an example implementing this caching strategy using the official StackExchange.Redis library in .NET.

using StackExchange.Redis;
using System.Text.Json;

public class CacheService
{
    private readonly IDatabase _db;

    public CacheService(IConnectionMultiplexer redis)
    {
        // Retrieve database instance from multiplexer
        _db = redis.GetDatabase();
    }

    public async Task GetUserAsync(int userId)
    {
        string key = $"user:{userId}";
        // Attempt fetching item string from Redis cache
        string cachedValue = await _db.StringGetAsync(key);

        if (!string.IsNullOrEmpty(cachedValue))
        {
            // Deserialize cached JSON back to target object
            return JsonSerializer.Deserialize(cachedValue);
        }

        // Fallback to database query on cache miss
        UserDto dbUser = await FetchUserFromDbAsync(userId);
        if (dbUser != null)
        {
            // Store result into Redis with 30-minute expiration
            string json = JsonSerializer.Serialize(dbUser);
            await _db.StringSetAsync(key, json, TimeSpan.FromMinutes(30));
        }
        return dbUser;
    }

    private async Task FetchUserFromDbAsync(int id)
    {
        // Simulated database call
        return await Task.FromResult(new UserDto { Id = id, Name = "John Doe" });
    }
}

public class UserDto
{
    public int Id { get; set; }
    public string Name { get; set; }
}

1 Answer


1

Introduction to the Cache-Aside Pattern

The Cache-Aside pattern, also known as Lazy Loading, is one of the most common strategies for integrating a cache with a database. In this approach, the application code is responsible for checking the cache before querying the database. If the data is missing (a cache miss), the application fetches it from the persistent store, populates the cache, and then returns it to the caller. This pattern keeps the application logic explicit and gives fine-grained control over cache invalidation.

Prerequisites

To follow this guide, you will need a running instance of Redis and the StackExchange.Redis NuGet package added to your C# .NET project.

Step-by-Step Implementation

Below is a complete service class that demonstrates how to read and write data using the Cache-Aside pattern. The class uses a ConnectionMultiplexer to manage the connection to Redis and encapsulates the logic for fetching user profiles.

Caching a User Profile by ID

When a request arrives, the service first attempts to retrieve the user from Redis. If the entry is absent, it queries the database (simulated here with an in-memory dictionary), writes the result back to Redis with a sliding expiration, and finally returns the data.

// Establish a connection to the local Redis instance.
ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost");
IDatabase cache = redis.GetDatabase();

// In-memory dictionary simulating a persistent database.
Dictionary<int, UserProfile> database = new()
{
    { 1, new UserProfile { Id = 1, Name = "Alice" } },
    { 2, new UserProfile { Id = 2, Name = "Bob" } }
};

public UserProfile? GetUserById(int userId)
{
    // Construct the cache key using a consistent naming convention.
    string cacheKey = $"user:{userId}";

    // Attempt to retrieve the serialized object from Redis.
    // Returns null if the key does not exist (cache miss).
    string? cachedValue = cache.StringGet(cacheKey);

    if (!string.IsNullOrEmpty(cachedValue))
    {
        // Deserialize the JSON string back into a UserProfile object.
        return JsonSerializer.Deserialize<UserProfile>(cachedValue);
    }

    // Cache miss: query the underlying database.
    if (database.TryGetValue(userId, out UserProfile? user))
    {
        // Serialize the retrieved object to JSON for storage.
        string serializedUser = JsonSerializer.Serialize(user);

        // Store the serialized object in Redis with a 5-minute sliding expiration.
        cache.StringSet(cacheKey, serializedUser, TimeSpan.FromMinutes(5));

        // Return the freshly fetched user to the caller.
        return user;
    }

    // Return null if the user does not exist in the database.
    return null;
}

Invalidating the Cache on Updates

Whenever the underlying data changes, you must explicitly remove the corresponding cache entry to prevent serving stale data. The following method demonstrates how to update a user profile and purge the stale cache record.

// Update the user in the persistent store.
public bool UpdateUser(int userId, UserProfile updatedUser)
{
    if (!database.ContainsKey(userId))
    {
        return false; // User not found.
    }

    database[userId] = updatedUser;

    // Remove the stale cache entry to ensure the next read fetches fresh data.
    string cacheKey = $"user:{userId}";
    cache.KeyDelete(cacheKey);

    return true;
}

By following this pattern, you ensure that your C# .NET application leverages Redis for high-performance reads while maintaining data consistency through explicit cache invalidation on writes. Remember to handle connection resilience and serialization overhead in production environments.

Write Your Answer