SkyWebFramework.Caching 1.0.0

dotnet add package SkyWebFramework.Caching --version 1.0.0
                    
NuGet\Install-Package SkyWebFramework.Caching -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SkyWebFramework.Caching" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SkyWebFramework.Caching" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="SkyWebFramework.Caching" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SkyWebFramework.Caching --version 1.0.0
                    
#r "nuget: SkyWebFramework.Caching, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SkyWebFramework.Caching@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SkyWebFramework.Caching&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=SkyWebFramework.Caching&version=1.0.0
                    
Install as a Cake Tool

🚀 SkyWebFramework.Caching

A 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.

NuGet Downloads License: MIT

✨ Features

🎯 Core Capabilities

  • Unified API - Single IEasyCache interface for all cache types
  • Memory Caching - Fast in-process caching with IMemoryCache
  • Distributed Caching - Support for Redis, SQL Server, and other distributed caches
  • Multi-Level Caching - Automatic L1 (Memory) + L2 (Distributed) with cache promotion
  • Thread-Safe Operations - Lock-free reads with semaphore-protected writes

🔥 Advanced Features

  • Scoped Contexts - Logical cache partitioning with automatic key prefixing
  • Compression - Automatic GZIP compression for distributed caches
  • Dual Serialization - Choose between System.Text.Json or Newtonsoft.Json
  • Built-in Metrics - Track cache hits, misses, and errors in real-time
  • GetOrSet Pattern - Atomic cache-aside pattern with factory functions
  • Flexible Expiration - Absolute, sliding, and relative expiration policies
  • Pattern Matching - Search and enumerate cache keys with wildcard patterns

🛡️ Production-Ready

  • Exception Handling - Configurable exception suppression
  • Logging Integration - Built-in logging with Microsoft.Extensions.Logging
  • Performance Optimized - Minimal allocations and lock contention
  • Async/Await - Fully asynchronous API

📦 Installation

dotnet add package SkyWebFramework.Caching

Or via NuGet Package Manager:

Install-Package SkyWebFramework.Caching

🚀 Quick Start

1. Memory Cache (L1)

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) 
            }
        );
    }
}

2. Distributed Cache (L2 - Redis)

// 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)
        );
    }
}

3. Multi-Level Cache (L1 + L2)

// 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)
        );
    }
}

🎯 Core API

Basic Operations

// 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)
);

Cache Entry Options

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);

🔥 Advanced Features

Scoped Cache Contexts

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();
    }
}

Nested Scopes

var userCache = cache.WithScope("users");
var adminCache = userCache.WithScope("admins");

// Key: "easycache:users:admins:123"
await adminCache.SetAsync("123", adminUser);

Cache Metrics

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)
        };
    }
}

Key Pattern Matching

// 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("*");

Compression (Distributed Only)

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

Custom Serialization

// 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;
});

🛠️ Extension Methods

GetOrSet Variations

// 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)
);

Set with Expiration

await cache.SetWithExpirationAsync("key", value, TimeSpan.FromMinutes(5));

Add (Set if not exists)

bool added = await cache.AddAsync("key", value, TimeSpan.FromMinutes(10));
if (added)
{
    Console.WriteLine("Value was added");
}
else
{
    Console.WriteLine("Key already exists");
}

Get and Remove (Pop)

var value = await cache.GetAndRemoveAsync<User>("temp-user");
// Value is retrieved and immediately removed

Bulk Operations

// 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)
});

🏗️ Cache Manager

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();

📊 Configuration Options

Global Cache Options

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;
}

Usage Example

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
    };
});

🎨 Common Patterns

1. Cache-Aside Pattern

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();
        }
    );
}

2. Refresh Ahead Pattern

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;
}

3. Write-Through Cache

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);
}

4. Write-Behind (Write-Back) Cache

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();
    });
}

5. Cache Invalidation on Update

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}");
}

6. Multi-Tenant Caching

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();
    }
}

🔧 Redis Setup

Using StackExchange.Redis

// 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);
});

Using Azure Redis Cache

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = Configuration.GetConnectionString("AzureRedis");
    options.InstanceName = "Production:";
});

Using SQL Server Cache

// 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();

📋 Best Practices

1. Use Appropriate Cache Types

// 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

2. Set Reasonable TTLs

// 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)
});

3. Use Key Prefixes

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

4. Monitor Cache Performance

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}");
    }
}

5. Handle Cache Failures Gracefully

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);
    }
}

6. Use Compression for Large Objects

builder.Services.AddEasyDistributedCache(options =>
{
    options.EnableCompression = true; // Compress objects > 1KB
});

// Good for:
// - Large JSON documents
// - Report data
// - Search results

🔍 Troubleshooting

Cache Not Working

// 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);
});

Redis Connection Issues

// 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}");
}

Performance Issues

// 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

📚 API Reference

IEasyCache Interface

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

Extension Methods

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

💡 Performance Tips

  1. Use Multi-Level Cache for best performance and reliability
  2. Enable Compression for large objects in distributed cache
  3. Set appropriate TTLs - not too short (cache thrashing), not too long (stale data)
  4. Use Scoped Caches to avoid key conflicts
  5. Monitor Metrics to identify cache effectiveness
  6. Batch Operations when possible to reduce round trips
  7. Use Memory Cache for hot data accessed frequently

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

💬 Support

🌟 Features Roadmap

  • Redis Cluster support
  • Cache warming strategies
  • Automatic cache invalidation
  • Cache tagging and bulk invalidation
  • Distributed lock support
  • Cache statistics dashboard
  • Memory cache size limits
  • Custom eviction policies

Made with ❤️ by the SkyWebFramework Team

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 213 11/9/2025

v1.0.0:
- Initial release
- Multi-level caching (L1 Memory + L2 Distributed)
- Scoped cache contexts for logical partitioning
- Automatic GZIP compression support
- Dual serialization (System.Text.Json and Newtonsoft.Json)
- Built-in cache hit/miss/error metrics
- Thread-safe GetOrSet with semaphore locking
- Unified IEasyCache interface
- Support for Memory, Distributed (Redis/SQL), and Multi-level caches