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

Persilsoft.Caching.Redis

NuGet Version NuGet Downloads License: MIT .NET

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

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

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