RedisCacheService 10.0.11

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

RedisCacheService

A production-ready, feature-rich Redis caching library for .NET 10.0 that simplifies Redis integration with automatic retry logic, connection resilience, compression, and comprehensive logging.

🚀 Features

  • Easy Integration - One-line setup with dependency injection
  • Built-in Resilience - Automatic retry logic with exponential backoff
  • Performance Optimized - Automatic compression for large values
  • Full Async Support - All operations support async/await
  • Enterprise Security - SSL/TLS support with certificate authentication
  • Comprehensive Logging - Built-in structured logging
  • Type-Safe Operations - Strongly-typed object caching with automatic JSON serialization
  • Multiple Data Types - Strings, Objects, Lists, and Sets (Groups)
  • Complete List Operations - 11+ Redis List operations including FIFO queues, LIFO stacks, pagination
  • GetOrSet Pattern - Cache-aside pattern built-in
  • Pagination Support - Efficient cursor-based pagination for large datasets
  • Atomic Operations - Transaction-based expiry setting prevents race conditions
  • Queue & Stack Support - Built-in FIFO (queue) and LIFO (stack) operations
  • Negative Indexing - Use negative indices for reverse access (e.g., -1 for last element)
  • Automatic Serialization - Objects automatically serialized/deserialized using System.Text.Json
  • Session Management - Complete session lifecycle management with automatic expiration
  • Distributed Locking - Distributed locks with auto-release and manual release options for critical sections

📦 Installation

Package Manager

Install-Package RedisCacheService

.NET CLI

dotnet add package RedisCacheService

PackageReference

<PackageReference Include="RedisCacheService" Version="10.0.0" />

⚙️ Configuration

1. Add Configuration to appsettings.json

{
  "Redis": {
    "Server": "localhost",
    "Port": 6379,
    "Password": "",
    "AbortOnConnectFail": false,
    "ConnectTimeout": 5000,
    "SyncTimeout": 5000,
    "KeepAlive": 30,
    "RetryCount": 3,
    "RetryDelayMilliseconds": 5000,
    "UseSsl": false,
    "SslHost": "",
    "CertificatePath": "",
    "CertificatePassword": "",
    "LockPrefix": "lock:",
    "DefaultLockExpiryMinutes": 60
  }
}

2. Register Services in Startup/Program.cs

For .NET 6+ (Minimal API)
using RedisCacheService;

var builder = WebApplication.CreateBuilder(args);

// Add Redis Cache Service
builder.Services.AddCache(builder.Configuration);

var app = builder.Build();
For .NET Core 3.1 / .NET 5
using RedisCacheService;

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Add Redis Cache Service
        services.AddCache(Configuration);
    }
}

3. Custom Configuration Section Name

If your configuration section has a different name:

services.AddCache(configuration, configSection: "MyRedisSettings");

4. Session Options

Pass session defaults (prefix and expiry) via RedisSessionOptions:

using RedisCacheService.Configuration;

services.AddCache(configuration, sessionOptions: new RedisSessionOptions
{
    SessionIdPrefix = "myapp",
    DefaultSessionExpiry = TimeSpan.FromMinutes(30)
});

🎯 Quick Start

Basic Usage

using RedisCacheService;

public class MyService
{
    private readonly RedisService _redis;
    
    public MyService(RedisService redis)
    {
        _redis = redis;
    }
    
    // Option 1: Manual cache check
    public async Task<string?> GetCachedDataAsync(string key)
    {
        // Get cached string
        var cached = await _redis.StringGetAsync(key);
        if (cached != null)
            return cached;
            
        // If not cached, fetch from source
        var data = await FetchFromSourceAsync();
        
        // Cache it for 1 hour
        await _redis.StringSetAsync(key, data, TimeSpan.FromHours(1));
        
        return data;
    }
    
    // Option 2: Use GetOrSet pattern (recommended - simpler)
    public async Task<string> GetCachedDataAsync(string key)
    {
        return await _redis.ObjectGetOrSetAsync(
            key,
            async () => await FetchFromSourceAsync(),
            TimeSpan.FromHours(1)
        );
    }
}

Quick Reference

Common Operations:

// Strings
await _redis.StringSetAsync("key", "value", TimeSpan.FromHours(1));
string? value = await _redis.StringGetAsync("key");

// Objects - single object (auto JSON serialization)
await _redis.ObjectSetAsync("user:123", user, TimeSpan.FromHours(1));
User? user = await _redis.ObjectGetSingleAsync<User>("user:123");

// Objects - list of objects
await _redis.ListSaveObjectsAsync("users", users, TimeSpan.FromHours(1));
List<User>? users = await _redis.ListGetObjectsAsync<User>("users");

// Lists - FIFO Queue (message queue)
await _redis.ListAddAsync("queue:tasks", task);          // Enqueue
Task? task = await _redis.ListFIFOAsync<Task>("queue:tasks"); // Dequeue

// Lists - LIFO Stack (undo/redo)
await _redis.ListAddAsync("stack:undo", state);          // Push
State? state = await _redis.ListLIFOAsync<State>("stack:undo"); // Pop

// Lists - Activity Feed
await _redis.ListInsertTopAsync("activities", activity);     // Add to top
List<Activity> recent = await _redis.ListGetRangeAsync<Activity>("activities", 0, 9); // Get first 10

// Groups (Sets) - Tags, Roles
await _redis.GroupAddAsync("tags", "technology");
bool hasTag = await _redis.GroupContainsAsync("tags", "technology");
List<string> allTags = await _redis.GroupGetAllAsStringAsync("tags");

// Sessions - User sessions with expiration (inject SessionService)
string? sessionId = await _sessionService.CreateSessionAsync(userData, TimeSpan.FromHours(2));
UserSession? session = await _sessionService.GetSessionAsync<UserSession>(sessionId);
await _sessionService.UpdateSessionAsync(sessionId, updatedData, TimeSpan.FromHours(2));
await _sessionService.DeleteSessionAsync(sessionId);

// Distributed Locking - Auto-release (recommended)
var result = await idempotencyService.LockAutoAsync("operation:123", async () => {
    // Critical section - only one execution at a time
    return await ProcessOperationAsync();
});

// Distributed Locking - Manual release
if (await idempotencyService.LockAcquireAsync("operation:123"))
{
    try
    {
        // Critical section code
    }
    finally
    {
        await idempotencyService.ReleaseLockAsync("operation:123");
    }
}

📚 Usage Examples

String Operations

Set and Get Strings
// Set a string value with expiration
bool success = await _redis.StringSetAsync(
    "user:123:name", 
    "John Doe", 
    TimeSpan.FromMinutes(30)
);

// Get a string value
string? name = await _redis.StringGetAsync("user:123:name");

// Synchronous versions
_redis.StringSet("key", "value", TimeSpan.FromHours(1));
string? value = _redis.StringGet("key");

Object Caching

Cache Objects with Automatic Serialization
public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

// Save a single object to cache
var user = new User { Id = 123, Name = "John", Email = "john@example.com" };
await _redis.ObjectSetAsync("user:123", user, TimeSpan.FromHours(1));

// Get a single object
User? cachedUser = await _redis.ObjectGetSingleAsync<User>("user:123");

// Save and get a list of objects
await _redis.ListSaveObjectsAsync("users:all", new List<User> { user1, user2 });
List<User> users = await _redis.ListGetObjectsAsync<User>("users:all");
GetOrSet Pattern (Cache-Aside)

This pattern automatically checks cache first, and if not found, executes your function and caches the result:

// Get from cache, or fetch from database and cache it
User user = await _redis.ObjectGetOrSetAsync(
    "user:123",
    async () => await _dbContext.Users.FindAsync(123),
    TimeSpan.FromHours(1)
);

// Synchronous version
User user = _redis.ObjectGetOrSet(
    "user:123",
    () => _dbContext.Users.Find(123),
    TimeSpan.FromHours(1)
);

List Operations

Redis Lists are ordered collections of string values, perfect for activity feeds, message queues, and ordered collections. All List operations support both string and generic object types (automatic JSON serialization).

Add to Lists
// Add a string to the end of a list
long length = await _redis.ListAddAsync(
    "recent:users", 
    "user123", 
    TimeSpan.FromDays(7)  // Optional: set expiry for the entire list
);

// Add an object to a list (automatically serialized to JSON)
var activity = new Activity { UserId = 123, Action = "Login" };
long length = await _redis.ListAddAsync("activities", activity, TimeSpan.FromHours(24));

// Add to the beginning of the list
await _redis.ListInsertTopAsync("recent:users", "user456");
await _redis.ListInsertTopAsync("activities", activity);

// Synchronous versions
long length = _redis.ListAdd("recent:users", "user123");
_redis.ListInsertTop("recent:users", "user456");
Get List Length
// Get the number of elements in a list
long count = await _redis.ListLengthAsync("recent:users");
Console.WriteLine($"List has {count} items");

// Synchronous version
long count = _redis.ListLength("recent:users");
Get Elements by Index
// Get element at specific index (0-based, negative for reverse)
string? first = await _redis.ListGetByIndexAsync("recent:users", 0);  // First element
string? last = await _redis.ListGetByIndexAsync("recent:users", -1);  // Last element
string? second = await _redis.ListGetByIndexAsync("recent:users", 1); // Second element

// Get and deserialize object by index
Activity? activity = await _redis.ListGetByIndexAsync<Activity>("activities", 0);

// Synchronous versions
string? first = _redis.ListGetByIndex("recent:users", 0);
Activity? activity = _redis.ListGetByIndex<Activity>("activities", 0);
Get Range of Elements (Pagination)
// Get first 10 elements (pagination for activity feeds)
List<string> first10 = await _redis.ListGetRangeAsync("recent:users", 0, 9);
List<string> last5 = await _redis.ListGetRangeAsync("recent:users", -5, -1);  // Last 5
List<string> all = await _redis.ListGetRangeAsync("recent:users", 0, -1);     // All elements

// Get range of objects (automatically deserialized)
List<Activity> activities = await _redis.ListGetRangeAsync<Activity>("activities", 0, 9);

// Synchronous versions
List<string> first10 = _redis.ListGetRange("recent:users", 0, 9);
List<Activity> activities = _redis.ListGetRange<Activity>("activities", 0, 9);
FIFO Queue Operations (First-In, First-Out)

Perfect for implementing message queues and task processing:

// Remove and return the first element (FIFO queue)
string? firstItem = await _redis.ListFIFOAsync("queue:tasks");
Task? firstTask = await _redis.ListFIFOAsync<Task>("queue:tasks");

// Synchronous versions
string? firstItem = _redis.ListFIFO("queue:tasks");
Task? firstTask = _redis.ListFIFO<Task>("queue:tasks");

// Example: Simple message queue
await _redis.ListAddAsync("queue:messages", "message1");
await _redis.ListAddAsync("queue:messages", "message2");

string? msg = await _redis.ListFIFOAsync("queue:messages");  // Returns "message1"
msg = await _redis.ListFIFOAsync("queue:messages");          // Returns "message2"
LIFO Stack Operations (Last-In, First-Out)

Perfect for implementing stacks and undo/redo functionality:

// Remove and return the last element (LIFO stack)
string? lastItem = await _redis.ListLIFOAsync("stack:items");
Item? lastItem = await _redis.ListLIFOAsync<Item>("stack:items");

// Synchronous versions
string? lastItem = _redis.ListLIFO("stack:items");
Item? lastItem = _redis.ListLIFO<Item>("stack:items");

// Example: Undo/Redo stack
await _redis.ListAddAsync("undo:stack", "state1");
await _redis.ListAddAsync("undo:stack", "state2");

string? state = await _redis.ListLIFOAsync("undo:stack");  // Returns "state2"
state = await _redis.ListLIFOAsync("undo:stack");          // Returns "state1"
Update Element by Index
// Update element at specific index (0-based, negative for reverse)
bool updated = await _redis.ListSetAsync("recent:users", 0, "updatedUser");
bool updated = await _redis.ListSetAsync("recent:users", -1, "updatedLastUser");

// Update object at index
User updatedUser = new User { Id = 123, Name = "Updated" };
bool updated = await _redis.ListSetAsync("users:list", 2, updatedUser);

// Synchronous versions
bool updated = _redis.ListSet("recent:users", 0, "updatedUser");
bool updated = _redis.ListSet("users:list", 2, updatedUser);
Insert Element Before/After Pivot
// Insert element before a pivot value
long position = await _redis.ListInsertAsync(
    "items:list", 
    "pivotItem", 
    "newItem", 
    beforePivot: true
);

// Insert element after a pivot value
long position = await _redis.ListInsertAsync(
    "items:list", 
    "pivotItem", 
    "newItem", 
    beforePivot: false
);

// Insert object before/after pivot object
long position = await _redis.ListInsertAsync("users:list", pivotUser, newUser, beforePivot: true);

// Returns: -1 if pivot not found, otherwise the length of the list after insertion

// Synchronous versions
long position = _redis.ListInsert("items:list", "pivotItem", "newItem", beforePivot: true);
Remove Elements by Value
// Remove all occurrences of a value
long removed = await _redis.ListRemoveAsync("items:list", "itemToRemove", count: 0);

// Remove first 2 occurrences
long removed = await _redis.ListRemoveAsync("items:list", "itemToRemove", count: 2);

// Remove last 2 occurrences (use negative count)
long removed = await _redis.ListRemoveAsync("items:list", "itemToRemove", count: -2);

// Remove object occurrences
long removed = await _redis.ListRemoveAsync<User>("users:list", userToRemove, count: 0);

// Returns: Number of elements removed

// Synchronous versions
long removed = _redis.ListRemove("items:list", "itemToRemove", count: 0);
Trim List (Keep Only N Items)
// Keep only first 100 items (remove all others)
bool trimmed = await _redis.ListTrimAsync("recent:users", 0, 99);

// Keep only last 100 items
bool trimmed = await _redis.ListTrimAsync("recent:users", -100, -1);

// Convenience method: Keep last N items
bool trimmed = await _redis.ListTrimToLastAsync("recent:users", 100);

// Synchronous versions
bool trimmed = _redis.ListTrim("recent:users", 0, 99);
bool trimmed = _redis.ListTrimToLast("recent:users", 100);
Save and Retrieve Lists of Objects

These methods store entire lists as JSON strings (not as Redis Lists). Use when you need to cache complete lists:

// Save a list of objects
var users = new List<User> { user1, user2, user3 };
bool saved = await _redis.ListSaveObjectsAsync("users:active", users, TimeSpan.FromHours(1));

// Get list with fallback to database (cache-aside pattern)
List<User> users = await _redis.ListGetObjectsAsync<User>(
    "users:active",
    async () => await _dbContext.Users.Where(u => u.IsActive).ToListAsync(),
    TimeSpan.FromMinutes(30)
);

// Synchronous versions
bool saved = _redis.ListSaveObjects("users:active", users);
List<User> users = _redis.ListGetObjects<User>("users:active", () => _dbContext.Users.ToList());
Complete List Operations Example
// Activity feed implementation
public class ActivityFeedService
{
    private readonly RedisService _redis;
    
    public async Task AddActivityAsync(Activity activity)
    {
        // Add new activity to the beginning of the list
        await _redis.ListInsertTopAsync("activities", activity, TimeSpan.FromDays(30));
        
        // Keep only last 1000 activities
        await _redis.ListTrimToLastAsync("activities", 1000);
    }
    
    public async Task<List<Activity>> GetRecentActivitiesAsync(int count = 20)
    {
        // Get most recent activities
        return await _redis.ListGetRangeAsync<Activity>("activities", 0, count - 1);
    }
    
    public async Task<Activity?> GetActivityByIndexAsync(long index)
    {
        return await _redis.ListGetByIndexAsync<Activity>("activities", index);
    }
    
    public async Task<long> GetActivityCountAsync()
    {
        return await _redis.ListLengthAsync("activities");
    }
    
    public async Task RemoveActivityAsync(Activity activity)
    {
        // Remove all occurrences of this activity
        await _redis.ListRemoveAsync("activities", activity, count: 0);
    }
}

// Message queue implementation
public class MessageQueueService
{
    private readonly RedisService _redis;
    
    public async Task EnqueueMessageAsync<T>(string queueName, T message) where T : class
    {
        await _redis.ListAddAsync($"queue:{queueName}", message);
    }
    
    public async Task<T?> DequeueMessageAsync<T>(string queueName) where T : class
    {
        // FIFO: Remove from left (first element)
        return await _redis.ListFIFOAsync<T>($"queue:{queueName}");
    }
    
    public async Task<int> GetQueueLengthAsync(string queueName)
    {
        return (int)await _redis.ListLengthAsync($"queue:{queueName}");
    }
}

Group Operations (Redis Sets)

Groups are unordered collections of unique string values, perfect for tags, roles, and unique collections.

Add to Groups
// Add a single value to a group
bool added = await _redis.GroupAddAsync("article:123:tags", "technology");

// Add multiple values at once (more efficient than multiple single adds)
string[] tags = { "technology", "programming", "csharp" };
long addedCount = await _redis.GroupAddMultipleAsync("article:123:tags", tags);

// Add with expiration (atomic operation - uses Redis transactions)
// The expiry is set atomically with the add operation to prevent race conditions
await _redis.GroupAddAsync("temp:users", "user123", TimeSpan.FromHours(1));

// Add multiple values with expiration
await _redis.GroupAddMultipleAsync("session:users", userIds, TimeSpan.FromMinutes(30));

Note: Expiry operations are atomic - the group expiry is set atomically with the add operation using Redis transactions, ensuring no race conditions.

Check Membership
// Check if a value exists in a group
bool hasTag = await _redis.GroupContainsAsync("article:123:tags", "technology");
if (hasTag)
{
    Console.WriteLine("Article has technology tag");
}
Get All Members
// Get all members as strings (for small groups - loads everything into memory)
List<string> tags = await _redis.GroupGetAllAsStringAsync("article:123:tags");

// Get all members as integers (for small groups)
List<int> userIds = await _redis.GroupGetAllAsIntAsync("active:users");

// Get count without loading all members (efficient)
long count = await _redis.GroupCountAsync("article:123:tags");

// For large groups, use pagination instead (see Pagination section below)
Pagination for Large Groups (Cursor-Based)

The library uses Redis SCAN cursors for efficient pagination. This is the recommended approach for large groups to avoid loading all members into memory.

// Iterate through large groups efficiently using cursor-based pagination
long cursor = 0;
do
{
    var result = await _redis.GroupGetAllAsStringAsync(
        "large:group", 
        cursor,      // Start with 0, then use result.Cursor for next iteration
        pageSize: 100,
        pattern: null  // Optional: filter by pattern (e.g., "user:*")
    );
    
    foreach (string member in result.Members)
    {
        // Process member
        Console.WriteLine(member);
    }
    
    // Use the cursor from result for next iteration
    cursor = result.Cursor;  // When cursor is 0, there are no more pages
} while (result.HasMore);  // Or check: result.Cursor != 0

// Alternative: Using GroupGetAll (returns RedisValue[])
long cursor = 0;
do
{
    var result = await _redis.GroupGetAllAsync("large:group", cursor, pageSize: 100);
    foreach (var member in result.Members)
    {
        Console.WriteLine(member.ToString());
    }
    cursor = result.Cursor;
} while (result.HasMore);

Key Points:

  • ✅ Uses actual Redis SCAN cursors (not page numbers) - efficient and safe
  • ✅ Start with cursor = 0 for a new scan
  • ✅ Use result.Cursor for the next iteration
  • ✅ When cursor == 0, there are no more pages
  • result.HasMore is a convenience property (equivalent to cursor != 0)
  • ✅ Supports pattern matching for filtering members
Set Operations
// Union - Get all unique members from multiple groups
string[] groups = { "article:1:tags", "article:2:tags", "article:3:tags" };
RedisValue[] allTags = await _redis.GroupUnionAsync(groups);

// Intersection - Get members that exist in ALL groups
RedisValue[] commonTags = await _redis.GroupIntersectionAsync(groups);

// Difference - Get members in first group but not in others
string[] others = { "article:2:tags", "article:3:tags" };
RedisValue[] uniqueTags = await _redis.GroupDifferenceAsync("article:1:tags", others);

// Move a member from one group to another
bool moved = await _redis.GroupMoveAsync("source:group", "dest:group", "value");

// Get random members
RedisValue[] randomTags = await _redis.GroupRandomMemberAsync("article:123:tags", count: 5);
Remove from Groups
// Remove a single value
bool removed = await _redis.GroupRemoveAsync("article:123:tags", "old-tag");

// Remove multiple values
string[] tagsToRemove = { "tag1", "tag2", "tag3" };
long removedCount = await _redis.GroupRemoveMultipleAsync("article:123:tags", tagsToRemove);

// Clear entire group
bool cleared = await _redis.GroupClearAsync("temp:group");

Distributed Locking

Distributed locking service for coordinating operations across multiple instances and preventing concurrent access to shared resources. Provides both auto-release and manual release options.

Auto-release locks automatically acquire the lock, execute your code, and release the lock even if an exception occurs:

using RedisCacheService;

public class MyService
{
    private readonly IdempotencyService _idempotencyService;
    
    public MyService(IdempotencyService idempotencyService)
    {
        _idempotencyService = idempotencyService;
    }
    
    // Execute function with auto-release lock
    public async Task<Result> ProcessOperationAsync(string operationId)
    {
        return await _idempotencyService.LockAutoAsync(operationId, async () => {
            // Critical section - only one execution at a time
            // Lock is automatically released after this code completes
            return await DoWorkAsync();
        });
    }
    
    // Execute action with auto-release lock
    public async Task ProcessDataAsync(string key)
    {
        await _idempotencyService.LockAutoAsync(key, async () => {
            // Critical section
            await UpdateDataAsync();
        });
    }
    
    // Synchronous versions
    public Result ProcessOperation(string operationId)
    {
        return _idempotencyService.LockAuto(operationId, () => {
            return DoWork();
        });
    }
}

Key Features:

  • ✅ Automatic lock acquisition and release
  • ✅ Lock released even on exceptions (uses finally block)
  • ✅ Expiry time is a safety net (default: 1 hour) - locks are released immediately after completion
  • ✅ Optional expiry parameter (defaults to configured value)
Manual Release Locks

For more control, you can manually acquire and release locks:

// Acquire lock
if (await _idempotencyService.LockAcquireAsync("operation:123"))
{
    try
    {
        // Critical section code
        await ProcessOperationAsync();
    }
    finally
    {
        // Always release the lock
        await _idempotencyService.ReleaseLockAsync("operation:123");
    }
}
else
{
    // Lock could not be acquired (already locked by another process)
    Console.WriteLine("Operation is currently locked");
}

// Synchronous versions
if (_idempotencyService.LockAcquire("operation:123"))
{
    try
    {
        ProcessOperation();
    }
    finally
    {
        _idempotencyService.ReleaseLock("operation:123");
    }
}
Check Lock Status
// Check if a lock exists
if (await _idempotencyService.IsLockedAsync("operation:123"))
{
    Console.WriteLine("Operation is currently locked");
}

// Synchronous version
if (_idempotencyService.IsLocked("operation:123"))
{
    Console.WriteLine("Operation is currently locked");
}
Configuration

Configure lock prefix and default expiry in appsettings.json:

{
  "Redis": {
    "LockPrefix": "myapp:lock:",
    "DefaultLockExpiryMinutes": 60
  }
}

Configuration Options:

  • LockPrefix - Prefix for all lock keys (default: "lock:")
  • DefaultLockExpiryMinutes - Default expiry time in minutes (default: 60). Note: Locks are released immediately after code execution - expiry is a safety net for crashes.
Complete Distributed Locking Example
public class PaymentService
{
    private readonly IdempotencyService _idempotencyService;
    
    public PaymentService(IdempotencyService idempotencyService)
    {
        _idempotencyService = idempotencyService;
    }
    
    // Process payment with distributed lock (prevents duplicate processing)
    public async Task<PaymentResult> ProcessPaymentAsync(string paymentId, PaymentRequest request)
    {
        return await _idempotencyService.LockAutoAsync($"payment:{paymentId}", async () => {
            // Only one payment processing at a time for this payment ID
            return await ExecutePaymentAsync(request);
        });
    }
    
    // Batch processing with lock
    public async Task ProcessBatchAsync(string batchId)
    {
        await _idempotencyService.LockAutoAsync($"batch:{batchId}", async () => {
            // Ensure only one instance processes this batch
            await ProcessBatchItemsAsync();
        });
    }
    
    // Manual lock for long-running operations
    public async Task<bool> StartLongOperationAsync(string operationId)
    {
        if (await _idempotencyService.LockAcquireAsync(operationId, TimeSpan.FromHours(2)))
        {
            try
            {
                // Long-running operation
                await PerformLongOperationAsync();
                return true;
            }
            finally
            {
                await _idempotencyService.ReleaseLockAsync(operationId);
            }
        }
        return false; // Already locked
    }
}

Key Features:

  • ✅ Auto-release locks (recommended) - automatic lock management
  • ✅ Manual release locks - for advanced control
  • ✅ Configurable lock prefix and default expiry
  • ✅ Lock status checking
  • ✅ Both synchronous and asynchronous methods
  • ✅ Safety net expiry (locks released immediately, expiry prevents deadlocks on crashes)

Session Management

Session management operations for creating, retrieving, updating, and managing user sessions in Redis. Sessions are stored as JSON objects with automatic expiration.

Note: Session operations use SessionService. Inject it via constructor: public MyService(SessionService sessionService).

Create Session
public class UserSession
{
    public int UserId { get; set; }
    public string Username { get; set; }
    public DateTime LastActivity { get; set; }
    public Dictionary<string, string> Properties { get; set; }
}

// Create a session with auto-generated session ID
var userSession = new UserSession 
{ 
    UserId = 123, 
    Username = "john", 
    LastActivity = DateTime.UtcNow 
};

string? sessionId = await _sessionService.CreateSessionAsync(
    userSession, 
    TimeSpan.FromHours(2)
);

if (sessionId != null)
{
    Console.WriteLine($"Session created: {sessionId}");
}

// Create a session with custom session ID
bool success = await _sessionService.CreateSessionAsync(
    "custom-session-123",
    userSession,
    TimeSpan.FromHours(2)
);

// Synchronous versions
string? sessionId = _sessionService.CreateSession(userSession, TimeSpan.FromHours(2));
bool success = _sessionService.CreateSession("custom-session-123", userSession, TimeSpan.FromHours(2));
Get Session
// Get session data by session ID
UserSession? session = await _sessionService.GetSessionAsync<UserSession>("session-id-123");

if (session != null)
{
    Console.WriteLine($"User ID: {session.UserId}");
    Console.WriteLine($"Username: {session.Username}");
}

// Synchronous version
UserSession? session = _sessionService.GetSession<UserSession>("session-id-123");
Update Session
// Update existing session data
var updatedSession = new UserSession 
{ 
    UserId = 123, 
    Username = "john", 
    LastActivity = DateTime.UtcNow 
};

bool success = await _sessionService.UpdateSessionAsync(
    "session-id-123",
    updatedSession,
    TimeSpan.FromHours(2)  // Optional: extend expiration
);

// Update without changing expiration (keeps existing expiry)
bool success = await _sessionService.UpdateSessionAsync(
    "session-id-123",
    updatedSession
);

// Synchronous version
bool success = _sessionService.UpdateSession("session-id-123", updatedSession, TimeSpan.FromHours(2));
Delete Session
// Delete a session
bool deleted = await _sessionService.DeleteSessionAsync("session-id-123");

if (deleted)
{
    Console.WriteLine("Session deleted successfully");
}

// Synchronous version
bool deleted = _sessionService.DeleteSession("session-id-123");
Check Session Exists
// Check if a session exists
if (await _sessionService.SessionExistsAsync("session-id-123"))
{
    var session = await _sessionService.GetSessionAsync<UserSession>("session-id-123");
    // Process session
}

// Synchronous version
if (_sessionService.SessionExists("session-id-123"))
{
    var session = _sessionService.GetSession<UserSession>("session-id-123");
}
Extend Session Expiration
// Extend the expiration time of an existing session
bool extended = await _sessionService.ExtendSessionAsync(
    "session-id-123",
    TimeSpan.FromHours(2)
);

if (extended)
{
    Console.WriteLine("Session expiration extended");
}

// Synchronous version
bool extended = _sessionService.ExtendSession("session-id-123", TimeSpan.FromHours(2));
Complete Session Management Example
public class AuthenticationService
{
    private readonly SessionService _sessionService;
    
    public AuthenticationService(SessionService sessionService)
    {
        _sessionService = sessionService;
    }
    
    public async Task<string?> LoginAsync(User user)
    {
        // Create session with user data
        var session = new UserSession
        {
            UserId = user.Id,
            Username = user.Username,
            LastActivity = DateTime.UtcNow,
            Properties = new Dictionary<string, string>
            {
                { "Role", user.Role },
                { "Email", user.Email }
            }
        };
        
        // Create session with 2 hour expiration
        var sessionId = await _sessionService.CreateSessionAsync(session, TimeSpan.FromHours(2));
        return sessionId;
    }
    
    public async Task<UserSession?> ValidateSessionAsync(string sessionId)
    {
        // Check if session exists
        if (!await _sessionService.SessionExistsAsync(sessionId))
        {
            return null;
        }
        
        // Get session data
        var session = await _sessionService.GetSessionAsync<UserSession>(sessionId);
        
        if (session != null)
        {
            // Update last activity and extend expiration
            session.LastActivity = DateTime.UtcNow;
            await _sessionService.UpdateSessionAsync(sessionId, session, TimeSpan.FromHours(2));
        }
        
        return session;
    }
    
    public async Task LogoutAsync(string sessionId)
    {
        // Delete session
        await _sessionService.DeleteSessionAsync(sessionId);
    }
    
    public async Task<bool> RefreshSessionAsync(string sessionId)
    {
        // Extend session expiration without updating data
        return await _sessionService.ExtendSessionAsync(sessionId, TimeSpan.FromHours(2));
    }
}

Key Features:

  • ✅ Automatic session ID generation using GUID
  • ✅ Configurable session key prefix (default: "session:")
  • ✅ Automatic JSON serialization/deserialization
  • ✅ Expiration time management
  • ✅ Session existence checking
  • ✅ Session expiration extension
  • ✅ Both synchronous and asynchronous methods

Connection Status

// Check if Redis is connected
if (_redis.IsConnected)
{
    // Perform Redis operations
}

// Get direct database access (advanced)
IDatabase db = _redis.GetDatabase(0); // 0 is default database

🔧 Advanced Configuration

SSL/TLS Configuration

For secure connections (e.g., Azure Redis Cache):

{
  "Redis": {
    "Server": "your-redis.redis.cache.windows.net",
    "Port": 6380,
    "Password": "your-password",
    "UseSsl": true,
    "SslHost": "your-redis.redis.cache.windows.net",
    "CertificatePath": "path/to/certificate.pfx",
    "CertificatePassword": "cert-password"
  }
}

Connection Resilience

The library automatically handles:

  • Connection failures with retry logic
  • Exponential backoff on retries
  • Graceful degradation when Redis is unavailable
  • Automatic reconnection

Configure retry behavior:

{
  "Redis": {
    "RetryCount": 5,
    "RetryDelayMilliseconds": 1000
  }
}

Compression

Large values are automatically compressed when enabled:

// Compression is enabled by default for values > 1KB
// You can configure this in the CacheOptions (if exposed in future versions)

🎨 Best Practices

1. Use Meaningful Key Names

// ✅ Good
await _redis.StringSetAsync("user:123:profile", data);
await _redis.StringSetAsync("product:456:details", data);

// ❌ Bad
await _redis.StringSetAsync("u123", data);
await _redis.StringSetAsync("p456", data);

2. Set Appropriate Expiration Times

// User data - 1 hour
await _redis.StringSetAsync("user:123", data, TimeSpan.FromHours(1));

// Session data - 30 minutes
await _redis.StringSetAsync("session:abc", data, TimeSpan.FromMinutes(30));

// Static reference data - 24 hours
await _redis.StringSetAsync("countries:list", data, TimeSpan.FromHours(24));

3. Use GetOrSet for Database Queries

// ✅ Good - Automatically handles cache miss
var user = await _redis.ObjectGetOrSetAsync(
    $"user:{userId}",
    async () => await _dbContext.Users.FindAsync(userId),
    TimeSpan.FromHours(1)
);

// ❌ Less efficient - Manual cache check
var cached = await _redis.ObjectGetSingleAsync<User>($"user:{userId}");
if (cached == null)
{
    cached = await _dbContext.Users.FindAsync(userId);
    await _redis.ObjectSetAsync($"user:{userId}", cached, TimeSpan.FromHours(1));
}

4. Handle Null Values

// Always check for null
string? value = await _redis.StringGetAsync("key");
if (value != null)
{
    // Use value
}

// For objects
User? user = await _redis.ObjectGetSingleAsync<User>("user:123");
if (user != null)
{
    // Use user
}

5. Use Cursor-Based Pagination for Large Datasets

// ✅ Good - Cursor-based pagination for large groups (uses Redis SCAN)
long cursor = 0;
do
{
    var result = await _redis.GroupGetAllAsStringAsync("large:group", cursor, pageSize: 100);
    // Process result.Members
    cursor = result.Cursor;  // Use actual Redis cursor for next iteration
} while (result.HasMore);  // When cursor is 0, no more pages

// ✅ Also Good - For small groups (< 1000 members), loading all is acceptable
List<string> all = await _redis.GroupGetAllAsStringAsync("small:group");

// ❌ Bad - Loading very large groups into memory
List<string> all = await _redis.GroupGetAllAsStringAsync("very:large:group");  // Could cause memory issues

When to use pagination:

  • Groups with > 1000 members
  • When memory usage is a concern
  • When processing members in batches
  • When you need pattern matching (filtering)

How cursor-based pagination works:

  • Uses Redis SSCAN command with actual cursors (not page numbers)
  • Efficient O(1) iteration, safe for concurrent modifications
  • Cursor 0 means no more pages
  • Supports pattern matching: GroupGetAllAsync("users", 0, 100, "user:*")

6. Use Cancellation Tokens

public async Task ProcessDataAsync(CancellationToken cancellationToken)
{
    var data = await _redis.StringGetAsync("key", cancellationToken);
    // Process data
}

🐛 Troubleshooting

Redis Connection Issues

If Redis is not connecting:

  1. Check Configuration

    // Verify your configuration is loaded correctly
    var redisConfig = Configuration.GetSection("Redis");
    var server = redisConfig["Server"];
    var port = redisConfig["Port"];
    
  2. Check Connection Status

    if (!_redis.IsConnected)
    {
        _logger.LogWarning("Redis is not connected");
        // Handle gracefully
    }
    
  3. Check Logs The library logs connection attempts, failures, and retries. Check your application logs for details.

Performance Issues

  1. Enable Compression - Automatically enabled for values > 1KB
  2. Use Appropriate Expiration - Don't cache forever
  3. Use Pagination - For large datasets
  4. Monitor Key Count - Too many keys can impact performance

Common Errors

"Redis connection error"

  • Check if Redis server is running
  • Verify connection string and credentials
  • Check firewall/network settings

"Key length exceeds maximum"

  • Default max key length is 512 bytes
  • Use shorter key names or configure max length

"Value size exceeds maximum cache size"

  • Default max value size is 1MB
  • Consider splitting large data or increasing limit

📖 API Reference

String Operations

Method Description
StringGet(string key) Get string value synchronously
StringGetAsync(string key) Get string value asynchronously
StringSet(string key, string value, TimeSpan? expiry) Set string value synchronously
StringSetAsync(string key, string value, TimeSpan? expiry) Set string value asynchronously

Object Operations

Method Description
ObjectGetSingle<T>(string key) Get a single object synchronously
ObjectGetSingleAsync<T>(string key) Get a single object asynchronously
ObjectGetOrSet<T>(string key, Func<T> getData, TimeSpan? expiry) Get or set object with fallback (sync)
ObjectGetOrSetAsync<T>(string key, Func<Task<T>> getData, TimeSpan? expiry) Get or set object asynchronously
ObjectSetAsync<T>(string key, T data, TimeSpan? expiry) Set a single object asynchronously
ObjectDelete(string key) Delete a cached object synchronously
ObjectDeleteAsync(string key) Delete a cached object asynchronously
ListSaveObjects<T>(string key, List<T> data, TimeSpan? expiry) Save list of objects
ListGetObjects<T>(string key, Func<List<T>>? getData, TimeSpan? expiry) Get list of objects with fallback

List Operations

Method Description
ListAdd(string key, string value, TimeSpan? expiry) Add string to end of list
ListAddAsync(string key, string value, TimeSpan? expiry) Add string to end of list asynchronously
ListAdd<T>(string key, T value, TimeSpan? expiry) Add object to end of list (auto-serialized)
ListAddAsync<T>(string key, T value, TimeSpan? expiry) Add object to end of list asynchronously
ListInsertTop(string key, string value, TimeSpan? expiry) Insert string at beginning of list
ListInsertTopAsync(string key, string value, TimeSpan? expiry) Insert string at beginning asynchronously
ListInsertTop<T>(string key, T value, TimeSpan? expiry) Insert object at beginning (auto-serialized)
ListInsertTopAsync<T>(string key, T value, TimeSpan? expiry) Insert object at beginning asynchronously
ListLength(string key) Get list length
ListLengthAsync(string key) Get list length asynchronously
ListGetByIndex(string key, long index) Get element by index (negative for reverse)
ListGetByIndexAsync(string key, long index) Get element by index asynchronously
ListGetByIndex<T>(string key, long index) Get and deserialize object by index
ListGetByIndexAsync<T>(string key, long index) Get and deserialize object by index asynchronously
ListGetRange(string key, long start, long stop) Get range of elements
ListGetRangeAsync(string key, long start, long stop) Get range of elements asynchronously
ListGetRange<T>(string key, long start, long stop) Get range of objects (auto-deserialized)
ListGetRangeAsync<T>(string key, long start, long stop) Get range of objects asynchronously
ListFIFO(string key) Remove and return first element (FIFO queue)
ListFIFOAsync(string key) Remove and return first element asynchronously
ListFIFO<T>(string key) Remove and return first object (auto-deserialized)
ListFIFOAsync<T>(string key) Remove and return first object asynchronously
ListLIFO(string key) Remove and return last element (LIFO stack)
ListLIFOAsync(string key) Remove and return last element asynchronously
ListLIFO<T>(string key) Remove and return last object (auto-deserialized)
ListLIFOAsync<T>(string key) Remove and return last object asynchronously
ListSet(string key, long index, string value) Update element at index
ListSetAsync(string key, long index, string value) Update element at index asynchronously
ListSet<T>(string key, long index, T value) Update object at index (auto-serialized)
ListSetAsync<T>(string key, long index, T value) Update object at index asynchronously
ListInsert(string key, string pivotValue, string value, bool beforePivot) Insert before/after pivot value
ListInsertAsync(string key, string pivotValue, string value, bool beforePivot) Insert before/after asynchronously
ListInsert<T>(string key, T pivotValue, T value, bool beforePivot) Insert object before/after pivot
ListInsertAsync<T>(string key, T pivotValue, T value, bool beforePivot) Insert object before/after asynchronously
ListRemove(string key, string value, long count) Remove elements by value (0 = all, positive = first N, negative = last N)
ListRemoveAsync(string key, string value, long count) Remove elements by value asynchronously
ListRemove<T>(string key, T value, long count) Remove objects by value
ListRemoveAsync<T>(string key, T value, long count) Remove objects by value asynchronously
ListTrim(string key, long start, long stop) Trim list to keep only specified range
ListTrimAsync(string key, long start, long stop) Trim list asynchronously
ListTrimToLast(string key, long keepCount) Keep only last N items (convenience method)
ListTrimToLastAsync(string key, long keepCount) Keep only last N items asynchronously
ListSaveObjects<T>(string key, List<T> data, TimeSpan? expiry) Save list of objects as JSON string
ListSaveObjectsAsync<T>(string key, List<T> data, TimeSpan? expiry) Save list of objects asynchronously
ListGetObjects<T>(string key, Func<List<T>>? getData, TimeSpan? expiry) Get list of objects with fallback
ListGetObjectsAsync<T>(string key, Func<Task<List<T>>>? getData, TimeSpan? expiry) Get list of objects asynchronously

Group Operations (Sets)

Method Description
GroupAdd(string groupName, string value, TimeSpan? expiry) Add value to group (atomic expiry)
GroupAddAsync(string groupName, string value, TimeSpan? expiry) Add value to group asynchronously (atomic expiry)
GroupAddMultiple(string groupName, string[] values, TimeSpan? expiry) Add multiple values (atomic expiry)
GroupAddMultipleAsync(string groupName, string[] values, TimeSpan? expiry) Add multiple values asynchronously (atomic expiry)
GroupRemove(string groupName, string value) Remove value from group
GroupContains(string groupName, string value) Check if value exists
GroupCount(string groupName) Get member count
GroupGetAllAsString(string groupName) Get all members as strings (for small groups)
GroupGetAllAsString(string groupName, long cursor, int pageSize, string? pattern) Get paginated members with cursor
GroupGetAllAsStringAsync(string groupName, long cursor, int pageSize, string? pattern) Get paginated members asynchronously
GroupGetAll(string groupName, long cursor, int pageSize, string? pattern) Get paginated members as RedisValue[]
GroupGetAllAsync(string groupName, long cursor, int pageSize, string? pattern) Get paginated members asynchronously
GroupUnion(string[] groupNames) Union of multiple groups
GroupIntersection(string[] groupNames) Intersection of groups
GroupDifference(string firstGroup, string[] otherGroups) Difference between groups
GroupMove(string sourceGroup, string destGroup, string value) Move member between groups
GroupClear(string groupName) Clear entire group

Session Management Operations

Method Description
CreateSession<T>(T sessionData, TimeSpan expiry, string sessionIdPrefix) Create session with auto-generated ID
CreateSessionAsync<T>(T sessionData, TimeSpan expiry, string sessionIdPrefix) Create session asynchronously with auto-generated ID
CreateSession<T>(string sessionId, T sessionData, TimeSpan expiry, string sessionIdPrefix) Create session with custom session ID
CreateSessionAsync<T>(string sessionId, T sessionData, TimeSpan expiry, string sessionIdPrefix) Create session asynchronously with custom session ID
GetSession<T>(string sessionId, string sessionIdPrefix) Get session data by session ID
GetSessionAsync<T>(string sessionId, string sessionIdPrefix) Get session data asynchronously
UpdateSession<T>(string sessionId, T sessionData, TimeSpan? expiry, string sessionIdPrefix) Update existing session data
UpdateSessionAsync<T>(string sessionId, T sessionData, TimeSpan? expiry, string sessionIdPrefix) Update session data asynchronously
DeleteSession(string sessionId, string sessionIdPrefix) Delete a session
DeleteSessionAsync(string sessionId, string sessionIdPrefix) Delete a session asynchronously
SessionExists(string sessionId, string sessionIdPrefix) Check if session exists
SessionExistsAsync(string sessionId, string sessionIdPrefix) Check if session exists asynchronously
ExtendSession(string sessionId, TimeSpan expiry, string sessionIdPrefix) Extend session expiration time
ExtendSessionAsync(string sessionId, TimeSpan expiry, string sessionIdPrefix) Extend session expiration asynchronously

Distributed Locking Operations

Method Description
LockAuto<T>(string key, Func<T> func, TimeSpan? expiry) Execute function with auto-release lock synchronously
LockAutoAsync<T>(string key, Func<Task<T>> func, TimeSpan? expiry) Execute function with auto-release lock asynchronously
LockAuto(string key, Action action, TimeSpan? expiry) Execute action with auto-release lock synchronously
LockAutoAsync(string key, Func<Task> action, TimeSpan? expiry) Execute action with auto-release lock asynchronously
LockAcquire(string key, TimeSpan? expiry) Acquire lock manually (requires ReleaseLock) synchronously
LockAcquireAsync(string key, TimeSpan? expiry) Acquire lock manually (requires ReleaseLock) asynchronously
ReleaseLock(string key) Release lock manually synchronously
ReleaseLockAsync(string key) Release lock manually asynchronously
IsLocked(string key) Check if lock exists synchronously
IsLockedAsync(string key) Check if lock exists asynchronously

📦 Publishing to NuGet

To build and publish the package:

# Build the package (creates .nupkg in bin/Debug or bin/Release)
dotnet build -c Release

# Push to NuGet.org (requires API key)
dotnet nuget push bin/Release/RedisCacheService.*.nupkg --api-key YOUR_API_KEY --source https://api.nuget.org/v3/index.json

Ensure Authors and Company in the .csproj are updated before publishing.

📄 License

Current Version (Free)

  • The current version of RedisCacheService is fully free and available for use without any restrictions.
  • All existing features, including caching operations, session management, distributed locking, and more, are available at no cost.

Future Plans (Q1 2026)

  • Starting in the first quarter of 2026, we will introduce new premium features and enhanced support that will be available under a paid license.
  • The current free version will continue to be available and maintained.
  • Paid plans will include:
    • Advanced features and capabilities
    • Priority support and technical assistance
    • Enhanced monitoring and analytics
    • Additional enterprise-grade features

Note: Existing users of the free version can continue using it without any changes. The transition to paid features is optional and only applies to new premium capabilities.

🙏 Acknowledgments

📞 Support

For issues or questions, please contact us:

  • Email: info@waelelazizy.com
  • Phone: +973 33030730

Made with ❤️ for the .NET Developers

#DotNet #Fintech #Redis #DistributedSystems #CSharp #PaymentProcessing

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
10.0.11 49 3/1/2026
10.0.10 62 2/15/2026
10.0.8 652 12/2/2025
10.0.7 653 12/2/2025
10.0.6 641 12/2/2025
10.0.5 163 11/23/2025
10.0.4 128 11/23/2025
10.0.3 141 11/23/2025
10.0.2 141 11/23/2025
10.0.1 132 11/23/2025
1.0.11 131 11/23/2025
1.0.10 132 11/23/2025
1.0.9 140 11/23/2025
1.0.8 139 11/23/2025
1.0.7 112 11/23/2025
1.0.6 99 11/23/2025
1.0.5 188 11/22/2025
1.0.4 370 11/20/2025
1.0.3 363 11/20/2025
1.0.2 374 11/20/2025