Persilsoft.Caching.Redis
1.0.0
dotnet add package Persilsoft.Caching.Redis --version 1.0.0
NuGet\Install-Package Persilsoft.Caching.Redis -Version 1.0.0
<PackageReference Include="Persilsoft.Caching.Redis" Version="1.0.0" />
<PackageVersion Include="Persilsoft.Caching.Redis" Version="1.0.0" />
<PackageReference Include="Persilsoft.Caching.Redis" />
paket add Persilsoft.Caching.Redis --version 1.0.0
#r "nuget: Persilsoft.Caching.Redis, 1.0.0"
#:package Persilsoft.Caching.Redis@1.0.0
#addin nuget:?package=Persilsoft.Caching.Redis&version=1.0.0
#tool nuget:?package=Persilsoft.Caching.Redis&version=1.0.0
Persilsoft.Caching.Redis
High-performance Redis implementation for Persilsoft.Caching.Abstractions with comprehensive feature support and flexible configuration options.
🎯 Overview
Persilsoft.Caching.Redis provides a production-ready Redis caching provider that implements ICacheProvider and IRedisSetOperations interfaces. Built on StackExchange.Redis with integrated logging, multi-tenant support, and robust error handling.
Perfect for building scalable distributed caching solutions in ASP.NET Core, microservices, and enterprise applications.
✨ Features
- ✅ Complete ICacheProvider Implementation - All 9 core caching operations
- ✅ Redis Set Operations - Efficient Set operations via
IRedisSetOperations(5 methods) - ✅ Flexible Configuration - 5 different configuration methods
- ✅ Multi-Tenant Support - InstanceName prefixing for isolated caches
- ✅ Batch Operations -
GetMany/RemoveManyfor improved performance - ✅ JSON Serialization - Automatic serialization for complex objects
- ✅ Integrated Logging - Comprehensive
ILoggersupport - ✅ Argument Validation - Robust error handling with
ArgumentNullException - ✅ Production-Ready - Battle-tested in enterprise environments
- ✅ Built on StackExchange.Redis - Industry-standard Redis client
📦 Installation
# Install Redis implementation
dotnet add package Persilsoft.Caching.Redis
# Install abstractions (if not already installed)
dotnet add package Persilsoft.Caching.Abstractions
Package Manager Console
Install-Package Persilsoft.Caching.Redis
Install-Package Persilsoft.Caching.Abstractions
🚀 Quick Start
1. Configure in appsettings.json
{
"Redis": {
"Configuration": "localhost:6379",
"InstanceName": "MyApp:"
}
}
2. Register in Program.cs
using Persilsoft.Caching.Redis;
var builder = WebApplication.CreateBuilder(args);
// Register Redis caching
builder.Services.AddRedisCaching(builder.Configuration);
var app = builder.Build();
app.Run();
3. Inject and use in services
using Persilsoft.Caching.Abstractions;
using Persilsoft.Caching.Abstractions.Models;
public class ProductService
{
private readonly ICacheProvider _cache;
public ProductService(ICacheProvider cache)
{
_cache = cache;
}
public async Task<Product?> GetProductAsync(int productId)
{
var key = $"product:{productId}";
// Try cache first
var cached = await _cache.GetAsync<Product>(key);
if (cached != null)
return cached;
// Cache miss - get from database
var product = await _db.Products.FindAsync(productId);
if (product != null)
{
// Cache for 1 hour
var policy = CacheItemPolicy.WithAbsoluteExpiration(TimeSpan.FromHours(1));
await _cache.SetAsync(key, product, policy);
}
return product;
}
}
That's it! You're now caching with Redis. 🚀
⚙️ Configuration Options
Option 1: From Configuration (Recommended)
appsettings.json:
{
"Redis": {
"Configuration": "localhost:6379,password=secret",
"InstanceName": "MyApp:"
}
}
Program.cs:
// Uses "Redis" section by default
builder.Services.AddRedisCaching(builder.Configuration);
// Or specify custom section
builder.Services.AddRedisCaching(builder.Configuration, "MyRedisSection");
Option 2: Simple Connection String
// Minimal configuration
builder.Services.AddRedisCaching("localhost:6379");
// With instance name for multi-tenant
builder.Services.AddRedisCaching("localhost:6379", instanceName: "Tenant1:");
// With password
builder.Services.AddRedisCaching("localhost:6379,password=secret");
Option 3: With Action<RedisCacheOptions>
using Microsoft.Extensions.Caching.StackExchangeRedis;
builder.Services.AddRedisCaching(options =>
{
options.Configuration = "localhost:6379,password=secret,ssl=true";
options.InstanceName = "MyApp:";
});
Option 4: With ConfigurationOptions (Advanced)
using StackExchange.Redis;
var configOptions = new ConfigurationOptions
{
EndPoints = { "localhost:6379", "localhost:6380" },
Password = "your-password",
ConnectTimeout = 5000,
SyncTimeout = 3000,
Ssl = true,
SslHost = "redis.example.com",
AbortOnConnectFail = false,
ConnectRetry = 3
};
builder.Services.AddRedisCaching(configOptions, instanceName: "MyApp:");
Option 5: With ConnectionMultiplexer Factory (Maximum Control)
using StackExchange.Redis;
builder.Services.AddRedisCaching(async () =>
{
var config = ConfigurationOptions.Parse("localhost:6379");
// Get password from Azure Key Vault or other secure source
config.Password = await GetPasswordFromVaultAsync();
// Custom connection logic
var connection = await ConnectionMultiplexer.ConnectAsync(config);
// Subscribe to events
connection.ConnectionFailed += (sender, args) =>
{
Console.WriteLine($"Redis connection failed: {args.Exception}");
};
return connection;
}, instanceName: "MyApp:");
💡 Usage Examples
Example 1: Basic String and Object Operations
public class CacheExamples
{
private readonly ICacheProvider _cache;
public async Task StringOperationsAsync()
{
// Set string with 30-minute expiration
await _cache.SetStringAsync("user:123:name", "John Doe",
CacheItemPolicy.WithAbsoluteExpiration(TimeSpan.FromMinutes(30)));
// Get string
var name = await _cache.GetStringAsync("user:123:name");
// Check existence
var exists = await _cache.ExistsAsync("user:123:name");
// Update expiration
await _cache.SetExpirationAsync("user:123:name", TimeSpan.FromHours(1));
// Remove
await _cache.RemoveAsync("user:123:name");
}
public async Task ObjectOperationsAsync()
{
var user = new User
{
Id = 123,
Name = "John",
Email = "john@example.com"
};
// Cache object (automatically serialized to JSON)
await _cache.SetAsync("user:123", user,
CacheItemPolicy.WithAbsoluteExpiration(TimeSpan.FromHours(1)));
// Retrieve object (automatically deserialized)
var cachedUser = await _cache.GetAsync<User>("user:123");
}
}
Example 2: Batch Operations for Performance
public class ProductCatalogService
{
private readonly ICacheProvider _cache;
private readonly IDbContext _db;
public async Task<Dictionary<int, Product>> GetProductsAsync(List<int> productIds)
{
// Generate keys
var keys = productIds.Select(id => $"product:{id}").ToList();
// Get multiple keys in ONE round-trip
var cachedItems = await _cache.GetManyAsync(keys);
var results = new Dictionary<int, Product>();
var missingIds = new List<int>();
for (int i = 0; i < productIds.Count; i++)
{
var key = keys[i];
if (cachedItems.TryGetValue(key, out var json) && json != null)
{
var product = JsonSerializer.Deserialize<Product>(json);
if (product != null)
results[productIds[i]] = product;
}
else
{
missingIds.Add(productIds[i]);
}
}
// Load missing products from database
if (missingIds.Any())
{
var dbProducts = await _db.Products
.Where(p => missingIds.Contains(p.Id))
.ToListAsync();
// Cache them for next time
foreach (var product in dbProducts)
{
var policy = CacheItemPolicy.WithAbsoluteExpiration(TimeSpan.FromHours(1));
await _cache.SetAsync($"product:{product.Id}", product, policy);
results[product.Id] = product;
}
}
return results;
}
public async Task ClearProductCacheAsync(List<int> productIds)
{
var keys = productIds.Select(id => $"product:{id}");
// Remove multiple keys in ONE operation
var removed = await _cache.RemoveManyAsync(keys);
Console.WriteLine($"Cleared {removed} product caches");
}
}
Example 3: Sliding Expiration for Sessions
public class SessionService
{
private readonly ICacheProvider _cache;
public async Task<UserSession?> GetOrCreateSessionAsync(string sessionId)
{
var key = $"session:{sessionId}";
var session = await _cache.GetAsync<UserSession>(key);
if (session == null)
{
// Create new session
session = new UserSession
{
SessionId = sessionId,
CreatedAt = DateTime.UtcNow
};
}
// Renew sliding expiration (30 minutes of inactivity)
var policy = CacheItemPolicy.WithSlidingExpiration(TimeSpan.FromMinutes(30));
await _cache.SetAsync(key, session, policy);
return session;
}
public async Task EndSessionAsync(string sessionId)
{
await _cache.RemoveAsync($"session:{sessionId}");
}
}
Example 4: Multi-Tenant Caching with InstanceName
// Program.cs - Each tenant gets isolated cache
var tenantId = builder.Configuration["TenantId"];
builder.Services.AddRedisCaching(options =>
{
options.Configuration = "localhost:6379";
options.InstanceName = $"Tenant{tenantId}:";
});
// Service - Keys are automatically prefixed
public class TenantService
{
private readonly ICacheProvider _cache;
public async Task SaveSettingsAsync(Settings settings)
{
// If InstanceName = "Tenant1:", actual Redis key will be "Tenant1:settings"
await _cache.SetAsync("settings", settings);
}
public async Task<Settings?> GetSettingsAsync()
{
// Automatically uses "Tenant1:settings"
return await _cache.GetAsync<Settings>("settings");
}
}
Example 5: Redis Set Operations (Online Users)
public class OnlineUsersService
{
private readonly IRedisSetOperations _redisOps;
public OnlineUsersService(IRedisSetOperations redisOps)
{
_redisOps = redisOps;
}
public async Task UserConnectedAsync(string userId)
{
await _redisOps.SetAddAsync("online:users", userId);
}
public async Task UserDisconnectedAsync(string userId)
{
await _redisOps.SetRemoveAsync("online:users", userId);
}
public async Task<IEnumerable<string>> GetOnlineUsersAsync()
{
return await _redisOps.SetMembersAsync("online:users");
}
public async Task<long> GetOnlineCountAsync()
{
return await _redisOps.SetLengthAsync("online:users");
}
public async Task<bool> IsUserOnlineAsync(string userId)
{
return await _redisOps.SetContainsAsync("online:users", userId);
}
}
Example 6: Cache-Aside Pattern with Write-Through
public class ProductRepository
{
private readonly ICacheProvider _cache;
private readonly IDbContext _db;
private readonly ILogger<ProductRepository> _logger;
public async Task<Product?> GetByIdAsync(int productId)
{
var key = $"product:{productId}";
// 1. Try cache first (Read-Through)
var cached = await _cache.GetAsync<Product>(key);
if (cached != null)
{
_logger.LogDebug("Cache hit for product {ProductId}", productId);
return cached;
}
// 2. Cache miss - get from database
_logger.LogDebug("Cache miss for product {ProductId}, querying database", productId);
var product = await _db.Products
.Include(p => p.Category)
.FirstOrDefaultAsync(p => p.Id == productId);
if (product != null)
{
// 3. Store in cache for future requests
var policy = CacheItemPolicy.WithAbsoluteExpiration(TimeSpan.FromHours(1));
await _cache.SetAsync(key, product, policy);
_logger.LogDebug("Cached product {ProductId}", productId);
}
return product;
}
public async Task UpdateAsync(Product product)
{
// 1. Update database (Write-Through)
_db.Products.Update(product);
await _db.SaveChangesAsync();
// 2. Update cache immediately
var key = $"product:{product.Id}";
var policy = CacheItemPolicy.WithAbsoluteExpiration(TimeSpan.FromHours(1));
await _cache.SetAsync(key, product, policy);
_logger.LogInformation("Updated and cached product {ProductId}", product.Id);
}
public async Task DeleteAsync(int productId)
{
// 1. Delete from database
var product = await _db.Products.FindAsync(productId);
if (product != null)
{
_db.Products.Remove(product);
await _db.SaveChangesAsync();
}
// 2. Invalidate cache
await _cache.RemoveAsync($"product:{productId}");
_logger.LogInformation("Deleted and invalidated cache for product {ProductId}", productId);
}
}
📝 Configuration Properties
When using RedisCacheOptions, you have access to these properties from Microsoft.Extensions.Caching.StackExchangeRedis:
public class RedisCacheOptions
{
// Connection string (e.g., "localhost:6379,password=secret")
public string? Configuration { get; set; }
// Advanced configuration object (alternative to Configuration string)
public ConfigurationOptions? ConfigurationOptions { get; set; }
// Factory for custom connection creation
public Func<Task<IConnectionMultiplexer>>? ConnectionMultiplexerFactory { get; set; }
// Prefix for all cache keys (useful for multi-tenant)
public string? InstanceName { get; set; }
// For Redis profiling
public Func<ProfilingSession>? ProfilingSession { get; set; }
}
🔗 Connection String Format
Redis connection strings support many options:
# Simple
localhost:6379
# With password
localhost:6379,password=secret
# With SSL
localhost:6379,ssl=true
# Custom timeout
localhost:6379,connectTimeout=5000
# Multiple endpoints (cluster)
redis1:6379,redis2:6379
# Don't fail on initial connect
localhost:6379,abortConnect=false
# Retry connection
localhost:6379,connectRetry=3
# Sync operation timeout
localhost:6379,syncTimeout=3000
# Full example
localhost:6379,password=secret,ssl=true,connectTimeout=5000,syncTimeout=3000,abortConnect=false
For all options, see StackExchange.Redis Configuration.
⚡ Performance Considerations
Use Batch Operations
// ❌ BAD - Multiple round trips
foreach (var id in productIds)
{
await _cache.GetAsync<Product>($"product:{id}");
}
// ✅ GOOD - Single round trip
var keys = productIds.Select(id => $"product:{id}");
var results = await _cache.GetManyAsync(keys);
Use InstanceName for Logical Separation
// ✅ GOOD - Prevents key collisions in shared Redis
options.InstanceName = "MyApp:";
// All keys prefixed automatically: "MyApp:user:123"
Choose Appropriate Expiration
// Frequently changing data - short TTL
CacheItemPolicy.WithAbsoluteExpiration(TimeSpan.FromMinutes(5));
// Rarely changing data - long TTL
CacheItemPolicy.WithAbsoluteExpiration(TimeSpan.FromHours(24));
// User session data - sliding expiration
CacheItemPolicy.WithSlidingExpiration(TimeSpan.FromMinutes(30));
Use Redis Sets for Collections
// ❌ BAD - Individual keys for each user
await _cache.SetStringAsync($"online:user:{userId}", "1");
// ✅ GOOD - Single Set for all users
await _redisOps.SetAddAsync("online:users", userId);
🐛 Error Handling
The library includes comprehensive error handling:
try
{
var value = await _cache.GetAsync<Product>("product:123");
}
catch (RedisConnectionException ex)
{
_logger.LogError(ex, "Failed to connect to Redis");
// Fallback to database or handle gracefully
}
catch (JsonException ex)
{
_logger.LogError(ex, "Failed to deserialize cached value");
// Invalidate corrupted cache entry
await _cache.RemoveAsync("product:123");
}
catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested)
{
_logger.LogWarning("Operation cancelled");
}
🧪 Testing
Unit Testing with Mocks
using Moq;
using Xunit;
public class ProductServiceTests
{
[Fact]
public async Task GetProduct_WhenCached_ReturnsCachedValue()
{
// Arrange
var mockCache = new Mock<ICacheProvider>();
var expectedProduct = new Product { Id = 1, Name = "Test" };
mockCache
.Setup(c => c.GetAsync<Product>("product:1", It.IsAny<CancellationToken>()))
.ReturnsAsync(expectedProduct);
var service = new ProductService(mockCache.Object);
// Act
var result = await service.GetProductAsync(1);
// Assert
Assert.Equal(expectedProduct.Id, result.Id);
mockCache.Verify(
c => c.GetAsync<Product>("product:1", It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task GetProduct_WhenNotCached_LoadsFromDatabase()
{
// Arrange
var mockCache = new Mock<ICacheProvider>();
mockCache
.Setup(c => c.GetAsync<Product>(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((Product?)null);
var service = new ProductService(mockCache.Object);
// Act
var result = await service.GetProductAsync(1);
// Assert
Assert.NotNull(result);
mockCache.Verify(
c => c.SetAsync(
"product:1",
It.IsAny<Product>(),
It.IsAny<CacheItemPolicy>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
}
Integration Testing with TestContainers
using Testcontainers.Redis;
using Xunit;
public class RedisIntegrationTests : IAsyncLifetime
{
private RedisContainer _redisContainer;
private ICacheProvider _cache;
public async Task InitializeAsync()
{
_redisContainer = new RedisBuilder().Build();
await _redisContainer.StartAsync();
var services = new ServiceCollection();
services.AddLogging();
services.AddRedisCaching(_redisContainer.GetConnectionString());
var provider = services.BuildServiceProvider();
_cache = provider.GetRequiredService<ICacheProvider>();
}
[Fact]
public async Task SetAndGet_WithRealRedis_WorksCorrectly()
{
// Arrange
var key = "test:key";
var value = "test value";
// Act
await _cache.SetStringAsync(key, value);
var result = await _cache.GetStringAsync(key);
// Assert
Assert.Equal(value, result);
}
public async Task DisposeAsync()
{
await _redisContainer.DisposeAsync();
}
}
📋 Requirements
- .NET 10.0 or later
- Redis Server 5.0+ (6.0+ recommended for best features)
📦 Dependencies
This package depends on:
Persilsoft.Caching.Abstractions- Core interfacesMicrosoft.Extensions.Caching.StackExchangeRedis- Redis supportMicrosoft.Extensions.Logging.Abstractions- LoggingMicrosoft.Extensions.DependencyInjection.Abstractions- DIMicrosoft.Extensions.Configuration.Abstractions- Configuration
🔗 Related Packages
- Persilsoft.Caching.Abstractions - Core abstractions
- Persilsoft.Storage.Abstractions - Object storage abstractions
📖 Documentation
🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
💬 Support
- 📧 Email: support@persilsoft.com
- 🐛 Issues: GitHub Issues
- 💡 Discussions: GitHub Discussions
🙏 Acknowledgments
- Built on StackExchange.Redis
- Inspired by
Microsoft.Extensions.Caching.StackExchangeRedis - Built with ❤️ for the .NET community
Made with ❤️ by Persilsoft
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.0
- Microsoft.Extensions.Caching.StackExchangeRedis (>= 10.0.0)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.0)
- Persilsoft.Caching.Abstractions (>= 1.0.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Persilsoft.Caching.Redis:
| Package | Downloads |
|---|---|
|
Persilsoft.Membership.RefreshTokenService.Caching.Redis
Contains a service for managing refresh tokens in Redis for the Membership project |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 42 | 12/14/2025 |
v1.0.0 - Initial Release (December 2025)
Core Implementation:
• Complete ICacheProvider implementation with Redis backend
- SetStringAsync/GetStringAsync for string operations
- SetAsync<T>/GetAsync<T> with automatic JSON serialization
- RemoveAsync, ExistsAsync, SetExpirationAsync for key management
- GetManyAsync, RemoveManyAsync for batch operations (single round-trip)
• IRedisSetOperations for Redis-specific features
- SetAddAsync - Add member to Set
- SetMembersAsync - Get all Set members
- SetRemoveAsync - Remove member from Set
- SetLengthAsync - Get Set cardinality
- SetContainsAsync - Check Set membership
- Enables efficient online users tracking, tags, categories, etc.
• Flexible Configuration (5 methods)
- Simple connection string: AddRedisCaching("localhost:6379")
- From IConfiguration: AddRedisCaching(configuration)
- With Action: AddRedisCaching(options => {...})
- With ConfigurationOptions: AddRedisCaching(configOptions)
- With Factory: AddRedisCaching(async () => {...})
• Multi-Tenant Support
- InstanceName property for key prefixing
- Isolate caches between tenants/apps
- Example: InstanceName = "Tenant1:" → keys become "Tenant1:user:123"
• Production Features
- Comprehensive logging with ILogger integration
- Argument validation (ArgumentNullException.ThrowIfNull)
- JSON serialization with error handling
- CancellationToken support on all async methods
- Connection resilience (AbortOnConnectFail = false)
Dependencies:
• Persilsoft.Caching.Abstractions (core interfaces)
• Microsoft.Extensions.Caching.StackExchangeRedis (Redis support)
• Microsoft.Extensions.Logging.Abstractions (logging)
• Microsoft.Extensions.DependencyInjection.Abstractions (DI)
• Microsoft.Extensions.Configuration.Abstractions (configuration)
Compatibility:
• .NET 8.0+
• Redis Server 5.0+ (6.0+ recommended)
• Fully async/await with CancellationToken
• Nullable reference types enabled
Usage Example:
// Configure in Program.cs
builder.Services.AddRedisCaching(builder.Configuration);
// Use in services
public class ProductService
{
private readonly ICacheProvider _cache;
public async Task<Product> GetProductAsync(int id)
{
var key = $"product:{id}";
var cached = await _cache.GetAsync<Product>(key);
if (cached != null) return cached;
var product = await _db.Products.FindAsync(id);
await _cache.SetAsync(key, product,
CacheItemPolicy.WithAbsoluteExpiration(TimeSpan.FromHours(1)));
return product;
}
}
Next Steps:
• Configure Redis connection in appsettings.json
• Register with AddRedisCaching() in Program.cs
• Inject ICacheProvider in your services
• Use IRedisSetOperations for Set operations when needed
Documentation:
• GitHub: https://github.com/persilsoft/Caching
• Samples: https://github.com/persilsoft/Caching/tree/main/samples
• Wiki: https://github.com/persilsoft/Caching/wiki