High-performance caching library for .NET with 7 unique features: Multi-Level Caching (L1/L2), Scoped Cache Contexts, Automatic Compression, Dual Serialization (System.Text.Json/Newtonsoft), Built-in Metrics, Lock-Free GetOrSet, and Unified API for Memory/Distributed/Redis. Perfect for building scalable applications with intelligent caching strategies.
$ dotnet add package SkyWebFramework.CachingA powerful, high-performance caching library for .NET that provides a unified API for memory, distributed, and multi-level caching with advanced features like automatic compression, metrics tracking, and scoped contexts.
IEasyCache interface for all cache typesdotnet add package SkyWebFramework.Caching
Or via NuGet Package Manager:
Install-Package SkyWebFramework.Caching
using SkyWebFramework.Caching;
// Register in Startup/Program.cs
builder.Services.AddMemoryCache();
builder.Services.AddEasyMemoryCache(options =>
{
options.DefaultTtl = TimeSpan.FromMinutes(30);
options.KeyPrefix = "myapp:";
});
// Use in your code
public class ProductService
{
private readonly IEasyCache _cache;
public ProductService(IEasyCache cache)
{
_cache = cache;
}
public async Task<Product> GetProductAsync(int id)
{
return await _cache.GetOrSetAsync(
$"product:{id}",
async () => await _database.GetProductAsync(id),
new CacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
}
);
}
}
// Register Redis
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
});
builder.Services.AddEasyDistributedCache(options =>
{
options.DefaultTtl = TimeSpan.FromHours(1);
options.EnableCompression = true; // Compress cached data
options.SerializerType = JsonSerializerType.SystemTextJson;
});
// Use distributed cache
public class UserService
{
private readonly IDistributedEasyCache _cache;
public UserService(IDistributedEasyCache cache)
{
_cache = cache;
}
public async Task<User> GetUserAsync(string userId)
{
// Cache across multiple servers
return await _cache.GetOrSetAsync(
$"user:{userId}",
async () => await _database.GetUserAsync(userId)
);
}
}
// Register both memory and distributed caches
builder.Services.AddMemoryCache();
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
});
builder.Services.AddEasyMultiLevelCache(options =>
{
options.DefaultTtl = TimeSpan.FromMinutes(30);
options.EnableCompression = true;
});
// Automatic cache promotion: L2 → L1
public class OrderService
{
private readonly IEasyCache _cache;
public OrderService(IEasyCache cache)
{
_cache = cache;
}
public async Task<Order> GetOrderAsync(int orderId)
{
// 1. Check L1 (memory) - fastest
// 2. Check L2 (Redis) - if found, promote to L1
// 3. Load from database - cache in both L1 and L2
return await _cache.GetOrSetAsync(
$"order:{orderId}",
async () => await _database.GetOrderAsync(orderId)
);
}
}
// Get value
var user = await cache.GetAsync<User>("user:123");
// Set value with expiration
await cache.SetAsync("user:123", user, new CacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15)
});
// Remove
await cache.RemoveAsync("user:123");
// Check existence
bool exists = await cache.ExistsAsync("user:123");
// Get or set (cache-aside pattern)
var product = await cache.GetOrSetAsync(
"product:456",
async () => await LoadProductFromDatabase(456)
);
var options = new CacheEntryOptions
{
// Expire after fixed time
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
// Expire if not accessed within time
SlidingExpiration = TimeSpan.FromMinutes(10),
// Absolute expiration timestamp
AbsoluteExpiration = DateTimeOffset.UtcNow.AddHours(2),
// Memory cache priority
Priority = CacheItemPriority.High,
// Size for memory cache limits
Size = 1024
};
await cache.SetAsync("key", value, options);
Create logical cache partitions without manual key management:
public class MultiTenantService
{
private readonly IEasyCache _cache;
public MultiTenantService(IEasyCache cache)
{
_cache = cache;
}
public async Task<Data> GetTenantDataAsync(string tenantId, string dataId)
{
// Create tenant-specific cache scope
var tenantCache = _cache.WithScope($"tenant:{tenantId}");
// Keys are automatically prefixed with "tenant:{tenantId}:"
return await tenantCache.GetOrSetAsync(
dataId, // Actually stored as "tenant:{tenantId}:{dataId}"
async () => await LoadData(tenantId, dataId)
);
}
public async Task ClearTenantCacheAsync(string tenantId)
{
var tenantCache = _cache.WithScope($"tenant:{tenantId}");
await tenantCache.ClearAsync();
}
}
var userCache = cache.WithScope("users");
var adminCache = userCache.WithScope("admins");
// Key: "easycache:users:admins:123"
await adminCache.SetAsync("123", adminUser);
Track cache performance in real-time:
public class CacheMonitor
{
private readonly IEasyCache _cache;
public CacheMonitor(IEasyCache cache)
{
_cache = cache;
}
public CacheStats GetStats()
{
var metrics = _cache.Metrics;
return new CacheStats
{
Hits = metrics.Hits,
Misses = metrics.Misses,
Errors = metrics.Errors,
HitRatio = (double)metrics.Hits / (metrics.Hits + metrics.Misses)
};
}
}
// Get all user keys
var userKeys = await cache.GetKeysAsync("user:*");
// Get keys with specific pattern
var adminKeys = await cache.GetKeysAsync("user:admin:*");
// Contains pattern
var searchKeys = await cache.GetKeysAsync("*search*");
// Get all keys
var allKeys = await cache.GetKeysAsync("*");
builder.Services.AddEasyDistributedCache(options =>
{
options.EnableCompression = true; // Enable GZIP compression
});
// Large objects are automatically compressed
await cache.SetAsync("large-report", reportData);
// Saves bandwidth and Redis memory
// Use System.Text.Json (default, faster)
builder.Services.AddEasyDistributedCache(options =>
{
options.SerializerType = JsonSerializerType.SystemTextJson;
options.JsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
WriteIndented = false
};
});
// Or use Newtonsoft.Json (more features)
builder.Services.AddEasyDistributedCache(options =>
{
options.SerializerType = JsonSerializerType.NewtonsoftJson;
});
// Simple version with TimeSpan
var data = await cache.GetOrSetAsync(
"key",
async () => await LoadData(),
TimeSpan.FromMinutes(10)
);
// With default value
var data = await cache.GetOrSetAsync(
"key",
async () => await LoadData(),
defaultValue: new Data(),
expiration: TimeSpan.FromMinutes(10)
);
await cache.SetWithExpirationAsync("key", value, TimeSpan.FromMinutes(5));
bool added = await cache.AddAsync("key", value, TimeSpan.FromMinutes(10));
if (added)
{
Console.WriteLine("Value was added");
}
else
{
Console.WriteLine("Key already exists");
}
var value = await cache.GetAndRemoveAsync<User>("temp-user");
// Value is retrieved and immediately removed
// Get multiple keys
var keys = new[] { "user:1", "user:2", "user:3" };
var users = await cache.GetMultipleAsync<User>(keys);
foreach (var (key, user) in users)
{
Console.WriteLine($"{key}: {user?.Name}");
}
// Set multiple keys
var values = new Dictionary<string, User>
{
["user:1"] = user1,
["user:2"] = user2,
["user:3"] = user3
};
await cache.SetMultipleAsync(values, new CacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15)
});
Manage multiple named caches:
// Register cache manager
builder.Services.AddEasyCacheManager();
// Register multiple named caches
var manager = serviceProvider.GetRequiredService<EasyCacheManager>();
manager.RegisterCache("products", productCache);
manager.RegisterCache("users", userCache);
manager.RegisterCache("sessions", sessionCache);
// Use named caches
var productCache = manager.GetCache("products");
await productCache.SetAsync("product:123", product);
// List all cache names
var cacheNames = manager.GetCacheNames();
// Clear all caches
await manager.ClearAllAsync();
public record CacheOptions
{
// Default time-to-live
public TimeSpan DefaultTtl { get; init; } = TimeSpan.FromMinutes(30);
// Enable compression (distributed only)
public bool EnableCompression { get; init; } = false;
// JSON serialization options
public JsonSerializerOptions? JsonOptions { get; init; }
// Key prefix for all cache entries
public string KeyPrefix { get; init; } = "easycache:";
// Serializer type (SystemTextJson or NewtonsoftJson)
public JsonSerializerType SerializerType { get; init; } = JsonSerializerType.SystemTextJson;
// Suppress cache exceptions (log but don't throw)
public bool SuppressExceptions { get; init; } = true;
}
builder.Services.AddEasyDistributedCache(options =>
{
options.DefaultTtl = TimeSpan.FromHours(2);
options.EnableCompression = true;
options.KeyPrefix = "myapp:v1:";
options.SuppressExceptions = false; // Throw exceptions in dev
options.SerializerType = JsonSerializerType.SystemTextJson;
options.JsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
});
public async Task<Product> GetProductAsync(int id)
{
return await _cache.GetOrSetAsync(
$"product:{id}",
async () =>
{
var product = await _db.Products.FindAsync(id);
return product ?? throw new NotFoundException();
}
);
}
public async Task<Data> GetWithRefreshAsync(string key)
{
var cached = await _cache.GetAsync<CachedData>(key);
if (cached != null)
{
// If data is older than 80% of TTL, refresh in background
var age = DateTime.UtcNow - cached.CachedAt;
if (age > TimeSpan.FromMinutes(8)) // 80% of 10 min
{
_ = Task.Run(async () =>
{
var fresh = await LoadFromSource();
await _cache.SetAsync(key, fresh);
});
}
return cached.Data;
}
// Cache miss - load and cache
var data = await LoadFromSource();
await _cache.SetAsync(key, new CachedData
{
Data = data,
CachedAt = DateTime.UtcNow
});
return data;
}
public async Task UpdateProductAsync(Product product)
{
// Update database
await _db.Products.UpdateAsync(product);
await _db.SaveChangesAsync();
// Update cache
await _cache.SetAsync($"product:{product.Id}", product);
}
public async Task UpdateProductAsync(Product product)
{
// Update cache immediately
await _cache.SetAsync($"product:{product.Id}", product);
// Update database in background
_ = Task.Run(async () =>
{
await _db.Products.UpdateAsync(product);
await _db.SaveChangesAsync();
});
}
public async Task UpdateUserAsync(User user)
{
await _db.Users.UpdateAsync(user);
await _db.SaveChangesAsync();
// Invalidate related cache entries
await _cache.RemoveAsync($"user:{user.Id}");
await _cache.RemoveAsync($"user:email:{user.Email}");
}
public class MultiTenantCacheService
{
private readonly IEasyCache _cache;
public async Task<T> GetTenantDataAsync<T>(string tenantId, string key)
{
var tenantCache = _cache.WithScope($"tenant:{tenantId}");
return await tenantCache.GetAsync<T>(key);
}
public async Task ClearTenantAsync(string tenantId)
{
var tenantCache = _cache.WithScope($"tenant:{tenantId}");
await tenantCache.ClearAsync();
}
}
// Install: dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
options.InstanceName = "MyApp:";
});
builder.Services.AddEasyDistributedCache(options =>
{
options.EnableCompression = true;
options.DefaultTtl = TimeSpan.FromMinutes(30);
});
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = Configuration.GetConnectionString("AzureRedis");
options.InstanceName = "Production:";
});
// Install: dotnet add package Microsoft.Extensions.Caching.SqlServer
builder.Services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = Configuration.GetConnectionString("CacheDb");
options.SchemaName = "dbo";
options.TableName = "CacheEntries";
});
builder.Services.AddEasyDistributedCache();
// Memory cache: Fast, but lost on restart
// - Session data
// - Frequently accessed read-only data
// - Small datasets
// Distributed cache: Shared across servers, persistent
// - User profiles
// - Product catalogs
// - API responses
// Multi-level: Best of both worlds
// - Critical data needing both speed and durability
// Short TTL for frequently changing data
await cache.SetAsync("stock-price", price, new CacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30)
});
// Long TTL for static data
await cache.SetAsync("country-list", countries, new CacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1)
});
// Sliding expiration for session data
await cache.SetAsync("user-session", session, new CacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(20)
});
builder.Services.AddEasyMemoryCache(options =>
{
options.KeyPrefix = $"{appName}:v{apiVersion}:";
});
// Keys: "myapp:v2:user:123", "myapp:v2:product:456"
// Easy to identify and version your cache entries
public class CacheHealthCheck : IHealthCheck
{
private readonly IEasyCache _cache;
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var metrics = _cache.Metrics;
var hitRatio = (double)metrics.Hits / (metrics.Hits + metrics.Misses);
if (hitRatio < 0.5)
{
return HealthCheckResult.Degraded(
$"Low cache hit ratio: {hitRatio:P}");
}
return HealthCheckResult.Healthy(
$"Cache hit ratio: {hitRatio:P}");
}
}
public async Task<Product> GetProductAsync(int id)
{
try
{
return await _cache.GetOrSetAsync(
$"product:{id}",
async () => await _db.GetProductAsync(id)
);
}
catch (Exception ex)
{
_logger.LogError(ex, "Cache error, falling back to database");
return await _db.GetProductAsync(id);
}
}
builder.Services.AddEasyDistributedCache(options =>
{
options.EnableCompression = true; // Compress objects > 1KB
});
// Good for:
// - Large JSON documents
// - Report data
// - Search results
// 1. Check if cache service is registered
var cache = serviceProvider.GetService<IEasyCache>();
if (cache == null)
{
Console.WriteLine("Cache not registered!");
}
// 2. Check if distributed cache is configured
var distributedCache = serviceProvider.GetService<IDistributedCache>();
if (distributedCache == null)
{
Console.WriteLine("Add Redis or SQL Server cache!");
}
// 3. Enable logging to see cache operations
builder.Services.AddLogging(config =>
{
config.AddConsole();
config.SetMinimumLevel(LogLevel.Debug);
});
// Test Redis connection
try
{
await cache.SetAsync("test", "value");
var result = await cache.GetAsync<string>("test");
Console.WriteLine(result == "value" ? "✓ Redis working" : "✗ Redis failed");
}
catch (Exception ex)
{
Console.WriteLine($"✗ Redis error: {ex.Message}");
}
// Check cache metrics
var metrics = cache.Metrics;
Console.WriteLine($"Hits: {metrics.Hits}");
Console.WriteLine($"Misses: {metrics.Misses}");
Console.WriteLine($"Errors: {metrics.Errors}");
Console.WriteLine($"Hit Ratio: {(double)metrics.Hits / (metrics.Hits + metrics.Misses):P}");
// Low hit ratio? Consider:
// - Increasing TTL
// - Using multi-level cache
// - Pre-warming cache on startup
| Method | Description |
|---|---|
GetAsync<T>(key) | Get cached value |
SetAsync<T>(key, value, options?) | Set cached value |
RemoveAsync(key) | Remove cached value |
ExistsAsync(key) | Check if key exists |
GetOrSetAsync<T>(key, factory, options?) | Get or compute and cache |
GetKeysAsync(pattern) | Get keys matching pattern |
ClearAsync() | Clear all cache entries |
WithScope(scopeName) | Create scoped cache view |
Metrics | Get cache metrics |
| Method | Description |
|---|---|
GetOrSetAsync<T>(key, factory, expiration) | Simplified GetOrSet |
SetWithExpirationAsync<T>(key, value, expiration) | Set with TTL |
AddAsync<T>(key, value, expiration?) | Add if not exists |
GetAndRemoveAsync<T>(key) | Get and remove atomically |
GetMultipleAsync<T>(keys) | Batch get operation |
SetMultipleAsync<T>(values, options?) | Batch set operation |
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.
Made with ❤️ by the SkyWebFramework Team