Redis Caching in .NET: Boost Your Application Performance
Backend14 min read

Redis Caching in .NET: Boost Your Application Performance

Implement distributed caching with Redis in .NET. Cache patterns, serialization, cache invalidation, and production best practices.

Taha Kocal

Taha Kocal

Full Stack Developer

Apr 2, 2025
#Redis#Caching#.NET#Performance#C#

Redis caching in .NET is a distributed caching strategy where application data is stored in a Redis in-memory data store shared across all app instances, dramatically reducing database load and response times. Redis delivers sub-millisecond reads and writes, offers rich data structures such as strings, hashes, sets, and sorted sets, and supports pub/sub for cache invalidation and cluster mode for horizontal scaling. In .NET, you integrate it either through the IDistributedCache abstraction (via the Microsoft.Extensions.Caching.StackExchangeRedis package) for simple get/set scenarios, or directly through StackExchange.Redis when you need advanced features like atomic counters, rate limiting, or pattern-based key invalidation. The most common approach is the cache-aside pattern: check the cache first, fall back to the database on a miss, then populate the cache with an expiration policy. This guide covers setup, a reusable cache service, invalidation patterns, rate limiting, session storage, and production best practices.

Why Redis?

Redis advantages:

  • Sub-millisecond latency for reads and writes
  • Distributed - shared cache across multiple app instances
  • Persistence options - survive restarts
  • Rich data structures - strings, hashes, lists, sets, sorted sets
  • Built-in pub/sub for cache invalidation
  • Cluster mode for horizontal scaling

Setup

bash
# Install packages
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
dotnet add package StackExchange.Redis

# Run Redis locally with Docker
docker run -d --name redis -p 6379:6379 redis:7-alpine
csharp
// Program.cs
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
    options.InstanceName = "MyApp_";
});

// appsettings.json
{
  "ConnectionStrings": {
    "Redis": "localhost:6379,abortConnect=false,connectTimeout=5000"
  }
}

Basic Usage with IDistributedCache

csharp
public class ProductService : IProductService
{
    private readonly IDistributedCache _cache;
    private readonly IProductRepository _repository;
    private readonly JsonSerializerOptions _jsonOptions;

    public ProductService(IDistributedCache cache, IProductRepository repository)
    {
        _cache = cache;
        _repository = repository;
        _jsonOptions = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };
    }

    public async Task<ProductDto?> GetProductAsync(Guid id, CancellationToken cancellationToken)
    {
        var cacheKey = $"product:{id}";

        // Try to get from cache
        var cachedData = await _cache.GetStringAsync(cacheKey, cancellationToken);
        if (cachedData != null)
        {
            return JsonSerializer.Deserialize<ProductDto>(cachedData, _jsonOptions);
        }

        // Get from database
        var product = await _repository.GetByIdAsync(id, cancellationToken);
        if (product == null) return null;

        var productDto = MapToDto(product);

        // Cache the result
        var options = new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10),
            SlidingExpiration = TimeSpan.FromMinutes(2)
        };

        await _cache.SetStringAsync(
            cacheKey,
            JsonSerializer.Serialize(productDto, _jsonOptions),
            options,
            cancellationToken);

        return productDto;
    }

    public async Task InvalidateProductCacheAsync(Guid id, CancellationToken cancellationToken)
    {
        await _cache.RemoveAsync($"product:{id}", cancellationToken);
    }
}

Generic Cache Service

csharp
public interface ICacheService
{
    Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default);
    Task SetAsync<T>(string key, T value, TimeSpan? expiration = null, CancellationToken cancellationToken = default);
    Task RemoveAsync(string key, CancellationToken cancellationToken = default);
    Task<T> GetOrSetAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiration = null, CancellationToken cancellationToken = default);
}

public class RedisCacheService : ICacheService
{
    private readonly IDistributedCache _cache;
    private readonly ILogger<RedisCacheService> _logger;
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };

    public RedisCacheService(IDistributedCache cache, ILogger<RedisCacheService> logger)
    {
        _cache = cache;
        _logger = logger;
    }

    public async Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default)
    {
        try
        {
            var data = await _cache.GetStringAsync(key, cancellationToken);
            if (data == null) return default;

            return JsonSerializer.Deserialize<T>(data, JsonOptions);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to get cache key {Key}", key);
            return default;
        }
    }

    public async Task SetAsync<T>(string key, T value, TimeSpan? expiration = null, CancellationToken cancellationToken = default)
    {
        try
        {
            var options = new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = expiration ?? TimeSpan.FromMinutes(5)
            };

            var data = JsonSerializer.Serialize(value, JsonOptions);
            await _cache.SetStringAsync(key, data, options, cancellationToken);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to set cache key {Key}", key);
        }
    }

    public async Task RemoveAsync(string key, CancellationToken cancellationToken = default)
    {
        try
        {
            await _cache.RemoveAsync(key, cancellationToken);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to remove cache key {Key}", key);
        }
    }

    public async Task<T> GetOrSetAsync<T>(
        string key,
        Func<Task<T>> factory,
        TimeSpan? expiration = null,
        CancellationToken cancellationToken = default)
    {
        var cached = await GetAsync<T>(key, cancellationToken);
        if (cached != null) return cached;

        var value = await factory();
        await SetAsync(key, value, expiration, cancellationToken);
        return value;
    }
}

What Is the Cache-Aside Pattern?

csharp
public class CachedUserRepository : IUserRepository
{
    private readonly IUserRepository _innerRepository;
    private readonly ICacheService _cache;
    private const string CachePrefix = "user:";
    private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(15);

    public CachedUserRepository(IUserRepository innerRepository, ICacheService cache)
    {
        _innerRepository = innerRepository;
        _cache = cache;
    }

    public async Task<User?> GetByIdAsync(Guid id, CancellationToken cancellationToken)
    {
        return await _cache.GetOrSetAsync(
            $"{CachePrefix}{id}",
            () => _innerRepository.GetByIdAsync(id, cancellationToken),
            CacheDuration,
            cancellationToken);
    }

    public async Task<User?> GetByEmailAsync(string email, CancellationToken cancellationToken)
    {
        return await _cache.GetOrSetAsync(
            $"{CachePrefix}email:{email.ToLowerInvariant()}",
            () => _innerRepository.GetByEmailAsync(email, cancellationToken),
            CacheDuration,
            cancellationToken);
    }

    public async Task UpdateAsync(User user, CancellationToken cancellationToken)
    {
        await _innerRepository.UpdateAsync(user, cancellationToken);

        // Invalidate cache
        await _cache.RemoveAsync($"{CachePrefix}{user.Id}", cancellationToken);
        await _cache.RemoveAsync($"{CachePrefix}email:{user.Email.ToLowerInvariant()}", cancellationToken);
    }
}

// Register with decoration pattern
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.Decorate<IUserRepository, CachedUserRepository>();

Direct Redis Access with StackExchange.Redis

csharp
// For advanced Redis features
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
    var configuration = builder.Configuration.GetConnectionString("Redis");
    return ConnectionMultiplexer.Connect(configuration!);
});

public class RedisService
{
    private readonly IConnectionMultiplexer _redis;
    private readonly IDatabase _db;

    public RedisService(IConnectionMultiplexer redis)
    {
        _redis = redis;
        _db = redis.GetDatabase();
    }

    // Hash operations - great for objects
    public async Task SetHashAsync(string key, Dictionary<string, string> fields)
    {
        var entries = fields.Select(f => new HashEntry(f.Key, f.Value)).ToArray();
        await _db.HashSetAsync(key, entries);
    }

    public async Task<Dictionary<string, string>> GetHashAsync(string key)
    {
        var entries = await _db.HashGetAllAsync(key);
        return entries.ToDictionary(e => e.Name.ToString(), e => e.Value.ToString());
    }

    // Sorted sets - great for leaderboards
    public async Task AddToLeaderboardAsync(string key, string member, double score)
    {
        await _db.SortedSetAddAsync(key, member, score);
    }

    public async Task<IEnumerable<(string Member, double Score)>> GetTopPlayersAsync(string key, int count)
    {
        var entries = await _db.SortedSetRangeByRankWithScoresAsync(key, 0, count - 1, Order.Descending);
        return entries.Select(e => (e.Element.ToString(), e.Score));
    }

    // Increment - atomic counter
    public async Task<long> IncrementAsync(string key)
    {
        return await _db.StringIncrementAsync(key);
    }

    // Set with expiration
    public async Task SetWithExpirationAsync(string key, string value, TimeSpan expiration)
    {
        await _db.StringSetAsync(key, value, expiration);
    }

    // Check if key exists
    public async Task<bool> ExistsAsync(string key)
    {
        return await _db.KeyExistsAsync(key);
    }
}

How Do You Invalidate Cache Entries?

Pattern-Based Invalidation

csharp
public class CacheInvalidationService
{
    private readonly IConnectionMultiplexer _redis;

    public CacheInvalidationService(IConnectionMultiplexer redis)
    {
        _redis = redis;
    }

    // Delete all keys matching a pattern
    public async Task InvalidateByPatternAsync(string pattern)
    {
        var endpoints = _redis.GetEndPoints();
        var server = _redis.GetServer(endpoints.First());
        var db = _redis.GetDatabase();

        var keys = server.Keys(pattern: pattern).ToArray();
        if (keys.Length > 0)
        {
            await db.KeyDeleteAsync(keys);
        }
    }

    // Invalidate all user-related cache
    public async Task InvalidateUserCacheAsync(Guid userId)
    {
        await InvalidateByPatternAsync($"*user:{userId}*");
    }

    // Invalidate all product cache
    public async Task InvalidateAllProductsAsync()
    {
        await InvalidateByPatternAsync("product:*");
    }
}

Pub/Sub for Distributed Invalidation

csharp
public class CacheInvalidationPublisher
{
    private readonly IConnectionMultiplexer _redis;
    private const string Channel = "cache-invalidation";

    public CacheInvalidationPublisher(IConnectionMultiplexer redis)
    {
        _redis = redis;
    }

    public async Task PublishInvalidationAsync(string cacheKey)
    {
        var subscriber = _redis.GetSubscriber();
        await subscriber.PublishAsync(Channel, cacheKey);
    }
}

public class CacheInvalidationSubscriber : BackgroundService
{
    private readonly IConnectionMultiplexer _redis;
    private readonly IMemoryCache _localCache;
    private readonly ILogger<CacheInvalidationSubscriber> _logger;

    public CacheInvalidationSubscriber(
        IConnectionMultiplexer redis,
        IMemoryCache localCache,
        ILogger<CacheInvalidationSubscriber> logger)
    {
        _redis = redis;
        _localCache = localCache;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var subscriber = _redis.GetSubscriber();

        await subscriber.SubscribeAsync("cache-invalidation", (channel, message) =>
        {
            var key = message.ToString();
            _localCache.Remove(key);
            _logger.LogDebug("Invalidated local cache key: {Key}", key);
        });

        // Keep the service running
        await Task.Delay(Timeout.Infinite, stoppingToken);
    }
}

How Do You Rate Limit with Redis?

csharp
public class RedisRateLimiter
{
    private readonly IDatabase _db;

    public RedisRateLimiter(IConnectionMultiplexer redis)
    {
        _db = redis.GetDatabase();
    }

    public async Task<bool> IsAllowedAsync(string key, int limit, TimeSpan window)
    {
        var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        var windowStart = now - (long)window.TotalSeconds;

        // Remove old entries
        await _db.SortedSetRemoveRangeByScoreAsync(key, 0, windowStart);

        // Count current entries
        var count = await _db.SortedSetLengthAsync(key);

        if (count >= limit)
        {
            return false;
        }

        // Add new entry
        await _db.SortedSetAddAsync(key, Guid.NewGuid().ToString(), now);
        await _db.KeyExpireAsync(key, window);

        return true;
    }
}

// Usage in middleware
public class RateLimitingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly RedisRateLimiter _rateLimiter;

    public async Task InvokeAsync(HttpContext context)
    {
        var clientIp = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
        var key = $"ratelimit:{clientIp}";

        if (!await _rateLimiter.IsAllowedAsync(key, limit: 100, window: TimeSpan.FromMinutes(1)))
        {
            context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
            await context.Response.WriteAsync("Rate limit exceeded");
            return;
        }

        await _next(context);
    }
}

Session Storage with Redis

csharp
// Program.cs
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(30);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
});

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

// Use in controller
[HttpPost("login")]
public async Task<IActionResult> Login(LoginRequest request)
{
    var user = await _authService.ValidateAsync(request);
    if (user == null) return Unauthorized();

    HttpContext.Session.SetString("UserId", user.Id.ToString());
    HttpContext.Session.SetString("UserEmail", user.Email);

    return Ok();
}

[HttpGet("profile")]
public IActionResult GetProfile()
{
    var userId = HttpContext.Session.GetString("UserId");
    if (string.IsNullOrEmpty(userId)) return Unauthorized();

    // ...
}

Best Practices

Cache best practices:

  • Use meaningful key prefixes: user:123, product:456
  • Always set expiration - avoid memory leaks
  • Handle cache misses gracefully - fallback to database
  • Don't cache user-specific data with shared keys
  • Use compression for large objects
  • Monitor cache hit rates
  • Implement circuit breaker for Redis failures
csharp
// Compression for large objects
public static class CacheExtensions
{
    public static byte[] Compress(this string data)
    {
        var bytes = Encoding.UTF8.GetBytes(data);
        using var output = new MemoryStream();
        using (var gzip = new GZipStream(output, CompressionLevel.Optimal))
        {
            gzip.Write(bytes, 0, bytes.Length);
        }
        return output.ToArray();
    }

    public static string Decompress(this byte[] data)
    {
        using var input = new MemoryStream(data);
        using var gzip = new GZipStream(input, CompressionMode.Decompress);
        using var output = new MemoryStream();
        gzip.CopyTo(output);
        return Encoding.UTF8.GetString(output.ToArray());
    }
}

Caching is not a silver bullet. Cache the right things: expensive computations, frequently accessed data, and data that doesn't change often. Always have a cache invalidation strategy and monitor your hit rates.

Redis caching can dramatically improve your application's performance and reduce database load. Start simple with IDistributedCache, then leverage advanced Redis features as your needs grow.

Share this article

Related Articles