Persiltech.Caching.Redis 1.0.2

dotnet add package Persiltech.Caching.Redis --version 1.0.2
                    
NuGet\Install-Package Persiltech.Caching.Redis -Version 1.0.2
                    
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="Persiltech.Caching.Redis" Version="1.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Persiltech.Caching.Redis" Version="1.0.2" />
                    
Directory.Packages.props
<PackageReference Include="Persiltech.Caching.Redis" />
                    
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 Persiltech.Caching.Redis --version 1.0.2
                    
#r "nuget: Persiltech.Caching.Redis, 1.0.2"
                    
#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 Persiltech.Caching.Redis@1.0.2
                    
#: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=Persiltech.Caching.Redis&version=1.0.2
                    
Install as a Cake Addin
#tool nuget:?package=Persiltech.Caching.Redis&version=1.0.2
                    
Install as a Cake Tool

Persiltech.Caching.Redis

NuGet Version NuGet Downloads License: MIT .NET

High-performance Redis implementation for Persiltech.Caching.Abstractions with comprehensive feature support and flexible configuration options.

🎯 Overview

Persiltech.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/RemoveMany for improved performance
  • JSON Serialization - Automatic serialization for complex objects
  • Integrated Logging - Comprehensive ILogger support
  • 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 Persiltech.Caching.Redis

# Install abstractions (if not already installed)
dotnet add package Persiltech.Caching.Abstractions

Package Manager Console

Install-Package Persiltech.Caching.Redis
Install-Package Persiltech.Caching.Abstractions

🚀 Quick Start

1. Configure in appsettings.json

{
  "Redis": {
    "Configuration": "localhost:6379",
    "InstanceName": "MyApp:"
  }
}

2. Register in Program.cs

using Persiltech.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 Persiltech.Caching.Abstractions;
using Persiltech.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

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:

  • Persiltech.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

📖 Documentation

🤝 Contributing

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

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/AmazingFeature)
  3. Commit your changes (git commit -m 'Add some AmazingFeature')
  4. Push to the branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

📄 License

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

💬 Support

🙏 Acknowledgments

  • Built on StackExchange.Redis
  • Inspired by Microsoft.Extensions.Caching.StackExchangeRedis
  • Built with ❤️ for the .NET community

Made with ❤️ by Persiltech

Product 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. 
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.2 90 5/15/2026
1.0.1 101 4/30/2026
1.0.0 100 4/30/2026

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:
     • Persiltech.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 10.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