This document provides detailed information about the Redis implementation and caching strategy used in "Copilot That Jawn".
The application uses Redis as a distributed cache to improve performance and scalability. Redis is integrated through .NET Aspire's orchestration system and provides multiple caching layers.
The application implements a sophisticated caching hierarchy:
- Local Memory Cache (L1) - 5-minute in-memory cache for hot data
- Redis Distributed Cache (L2) - 6-hour distributed cache for shared data
- Output Cache - Redis-backed page-level caching
- HTTP Response Cache - Client-side caching with appropriate headers
Request → Check L1 (Memory) → Check L2 (Redis) → Database → Cache Population
↓ ↓ ↓ ↓ ↑
Cache Hit Cache Hit Cache Hit Cache Miss Populate All Layers
Redis is configured in the AppHost project with the following setup:
// AppHost/AppHost.cs
var redis = builder.AddRedis("redis")
.WithRedisInsight() // Enable RedisInsight dashboard
.WithLifetime(ContainerLifetime.Persistent) // Persist data across restarts
.PublishAsConnectionString(); // Make connection available to services
var web = builder.AddProject<Web>("web")
.WithReference(redis) // Inject Redis connection
.WaitFor(redis); // Ensure Redis starts firstIn the Web project, Redis services are registered:
// Web/Program.cs
builder.AddRedisDistributedCache("redis"); // Distributed caching
builder.AddRedisOutputCache("redis"); // Output caching
// Configure Redis options
builder.Services.PostConfigure<RedisCacheOptions>(options =>
{
options.InstanceName = "CopilotThatJawn:"; // Key prefix for organization
});The primary caching implementation is in the ContentService class:
public class ContentService : IContentService
{
private const string TIPS_CACHE_KEY = "content_tips";
private static readonly TimeSpan _distributedCacheExpiry = TimeSpan.FromHours(6);
private static readonly TimeSpan _localCacheExpiry = TimeSpan.FromMinutes(5);
private async Task<List<TipModel>> GetTipsFromCacheAsync()
{
// L1: Check local memory cache first
if (_cache.TryGetValue(TIPS_CACHE_KEY, out List<TipModel>? localTips))
{
return localTips ?? new List<TipModel>();
}
// L2: Check Redis distributed cache
var distributedTipsJson = await _distributedCache.GetStringAsync(TIPS_CACHE_KEY);
List<TipModel> tips;
if (!string.IsNullOrEmpty(distributedTipsJson))
{
// Cache hit in Redis
tips = JsonSerializer.Deserialize<List<TipModel>>(distributedTipsJson) ?? new List<TipModel>();
_logger.LogDebug("Loaded {Count} tips from Redis cache", tips.Count);
}
else
{
// Cache miss - load from database
tips = await GetTipsFromAzureTableAsync();
// Store in Redis
var serializedTips = JsonSerializer.Serialize(tips);
var distributedCacheOptions = new DistributedCacheEntryOptions
{
SlidingExpiration = _distributedCacheExpiry
};
await _distributedCache.SetStringAsync(TIPS_CACHE_KEY, serializedTips, distributedCacheOptions);
_logger.LogInformation("Cached {Count} tips in Redis", tips.Count);
}
// Store in local memory cache
_cache.Set(TIPS_CACHE_KEY, tips, _localCacheExpiry);
return tips;
}
}The application defines several output cache policies:
// Configure output cache policies
builder.Services.Configure<OutputCacheOptions>(options =>
{
// Default policy: 6-hour cache
options.AddBasePolicy(builder =>
builder.Cache()
.SetVaryByHost(true)
.SetVaryByQuery("*")
.Expire(TimeSpan.FromHours(6))
.Tag("outputcache", "site"));
// Static content: 3-day cache
options.AddPolicy("StaticContent", builder =>
builder.Cache()
.SetVaryByHost(true)
.Expire(TimeSpan.FromDays(3))
.Tag("outputcache", "static"));
// Tips content: 3-day cache with slug variation
options.AddPolicy("TipsContent", builder =>
builder.Cache()
.SetVaryByHost(true)
.SetVaryByRouteValue("slug")
.Expire(TimeSpan.FromDays(3))
.Tag("outputcache", "tips", "content"));
});When running the application locally:
- Automatic Redis Setup: Aspire automatically starts a Redis container
- RedisInsight Dashboard: Available through the Aspire dashboard for cache inspection
- Persistent Storage: Redis data persists across application restarts
- Connection Management: Connection strings are automatically configured
The Aspire dashboard provides:
- Service Status: Monitor Redis container health
- Connection Strings: View Redis connection details
- Logs: Redis server logs and application cache activity
- Metrics: Cache hit/miss ratios and performance metrics
RedisInsight is automatically available and provides:
- Key Browser: Explore cached keys and their values
- Performance Monitoring: Monitor Redis performance metrics
- Memory Usage: Track Redis memory consumption
- Command Analysis: Analyze Redis commands and their performance
The application provides several methods for cache invalidation:
public async Task InvalidateTipsCacheAsync()
{
// Clear distributed cache
await _distributedCache.RemoveAsync(TIPS_CACHE_KEY);
// Clear local memory cache
_cache.Remove(TIPS_CACHE_KEY);
// Clear output cache for tips pages
await _outputCacheStore.EvictByTagAsync("tips", default);
await _outputCacheStore.EvictByTagAsync("content", default);
}# Refresh cache via secured API endpoint
curl -X POST https://localhost:5001/api/cache/refresh \
-H "X-API-Key: your-api-key"The API endpoint provides comprehensive cache refresh:
- Refreshes content from the database
- Updates both local and distributed caches
- Invalidates output cache entries
- Returns detailed refresh status
When content is refreshed:
- Load Fresh Data: Retrieve latest content from Azure Table Storage
- Update Redis: Store serialized content in Redis with 6-hour expiration
- Update Local Cache: Store in memory cache with 5-minute expiration
- Invalidate Output Cache: Clear page-level caches using tags
- Log Activity: Record cache refresh operations for monitoring
In production, Redis connection is configured through:
- Environment Variables:
ConnectionStrings__rediscontains the Redis connection string - Azure Configuration: Connection strings managed through Azure App Configuration
- Secure Parameters: Sensitive connection details secured through Azure Key Vault
- Connection Pooling: StackExchange.Redis manages connection pooling automatically
- Serialization: JSON serialization for compatibility and performance
- Key Expiration: Sliding expiration policies to balance freshness and performance
- Memory Management: Appropriate eviction policies for memory-constrained environments
Production monitoring should include:
- Cache Hit Ratios: Monitor effectiveness of caching strategy
- Redis Memory Usage: Ensure Redis doesn't exceed memory limits
- Connection Health: Monitor Redis connectivity and latency
- Cache Refresh Frequency: Track how often content is being refreshed
# Check if Redis is running through Aspire
dotnet run --project AppHost --verbose
# Verify Redis container status in Aspire dashboard# Force cache refresh via API
curl -X POST https://localhost:5001/api/cache/refresh
# Check logs for cache invalidation- Monitor cache hit ratios in application logs
- Use RedisInsight to analyze key usage patterns
- Consider adjusting cache expiration times
Enable verbose logging for cache operations:
{
"Logging": {
"LogLevel": {
"Web.Services.ContentService": "Debug",
"Microsoft.Extensions.Caching": "Debug"
}
}
}- Consistent Prefixes: Use "content_" prefix for content-related keys
- Descriptive Names: Keys should clearly indicate their content
- Version Considerations: Include version information if cache format changes
- Sliding Expiration: Use sliding expiration for frequently accessed data
- Absolute Expiration: Use absolute expiration for time-sensitive content
- Layered TTL: Different TTL values for different cache layers
- Graceful Degradation: Application should work even if Redis is unavailable
- Fallback Strategies: Always have a fallback to database queries
- Retry Logic: Implement appropriate retry logic for Redis operations
| Variable | Description | Default |
|---|---|---|
ConnectionStrings__redis |
Redis connection string | Configured by Aspire |
CacheRefresh__ApiKey |
API key for cache refresh endpoint | Development: none, Production: required |
| Setting | Value | Purpose |
|---|---|---|
| Local Cache TTL | 5 minutes | Hot data caching |
| Redis Cache TTL | 6 hours | Distributed caching |
| Output Cache TTL | 3 days (content), 6 hours (dynamic) | Page-level caching |
| Redis Instance Name | "CopilotThatJawn:" | Key organization |
This configuration provides optimal performance while ensuring content freshness and system reliability.