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
# 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// 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
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
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?
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
// 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
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
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?
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
// 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
// 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.
