An open API service indexing awesome lists of open source software.

https://github.com/ersintarhan/rediskit

Production-ready Redis toolkit for .NET 9 with advanced caching, pub/sub, and streaming features
https://github.com/ersintarhan/rediskit

cache circuit-breaker csharp dotnet high-performance messagepack net9 pubsub redis streams

Last synced: 5 months ago
JSON representation

Production-ready Redis toolkit for .NET 9 with advanced caching, pub/sub, and streaming features

Awesome Lists containing this project

README

          

# RedisKit

[![Build Status](https://github.com/ersintarhan/RedisKit/workflows/CI/badge.svg)](https://github.com/ersintarhan/RedisKit/actions)
[![Code Quality](https://img.shields.io/badge/qodana-65%25%20coverage-brightgreen)](https://qodana.cloud/projects/ersintarhan-RedisKit)
[![Tests](https://img.shields.io/badge/tests-645%20passed-brightgreen)](https://github.com/ersintarhan/RedisKit/actions)
[![Coverage](https://img.shields.io/badge/coverage-65%25-brightgreen)](#testing)
[![.NET](https://img.shields.io/badge/.NET-9.0-blue)](https://dotnet.microsoft.com/)
[![NuGet](https://img.shields.io/nuget/v/RedisKit.svg)](https://www.nuget.org/packages/RedisKit/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Performance](https://img.shields.io/badge/performance-95%25%20faster-success)](#performance-benchmarks)

A production-ready, enterprise-grade Redis library for .NET 9 with advanced caching, pub/sub, and streaming features.

## ๐Ÿš€ Features

### Core Features

- **Caching**: Generic Get, Set, Delete operations with TTL support
- **Batch Operations**: GetMany and SetMany for improved performance
- **Key Prefixing**: Support for cache key prefixes
- **Pub/Sub**: Type-safe publishing and subscribing with advanced pattern matching
- **Streaming**: Redis Streams support with consumer groups and retry mechanisms
- **Multiple Serializers**: JSON, MessagePack support
- **Dependency Injection**: Full support with .NET DI container
- **High Performance Logging**: Source generator based logging with EventId support
- **Async/Await**: Full async/await support with CancellationToken

### Redis 7.x Features (NEW!)

- **๐Ÿš€ Redis Functions**: Server-side scripting with Lua (replacement for Redis Scripts)
- **๐Ÿ“ก Sharded Pub/Sub**: Scalable pub/sub across cluster shards
- **๐Ÿ”ง Function Library Builder**: Fluent API for creating Redis function libraries
- **๐Ÿ“Š Array Return Types**: Full support for array results from Redis functions
- **๐ŸŽฏ Native Sharded Channel Support**: Using StackExchange.Redis's RedisChannel.Sharded() API

### Enterprise Features

- **๐Ÿ”’ Distributed Locking**: Redis-based distributed locking with auto-renewal
- **๐Ÿ›ก๏ธ Redis Sentinel Support**: High availability with automatic failover (NEW!)
- **๐Ÿ”„ Circuit Breaker Pattern**: Automatic failure detection and recovery
- **๐Ÿ“ˆ Advanced Retry Strategies**: Multiple backoff strategies (Exponential, Decorrelated Jitter, etc.)
- **๐Ÿฅ Health Monitoring**: Automatic health checks with auto-reconnection
- **๐ŸŽฏ Pattern Matching**: Redis glob pattern support (`*`, `?`, `[abc]`, `[^abc]`, `[a-z]`)
- **๐Ÿงน Memory Leak Prevention**: Automatic cleanup of inactive handlers
- **๐Ÿ“Š Statistics & Monitoring**: Built-in metrics for subscriptions and connections
- **โšก High Performance**: Optimized with concurrent collections and minimal allocations
- **๐Ÿ” Thread Safety**: All operations are thread-safe
- **๐Ÿš€ Lua Script Optimization**: 90-95% performance improvement for batch operations
- **๐Ÿ“ Source-Generated Logging**: Zero-allocation high-performance logging

### Performance & Memory Optimizations

- **๐Ÿ’พ Object Pooling**: ArrayPool and ObjectPool for reduced GC pressure
- **๐ŸŒŠ Streaming API**: IAsyncEnumerable for processing large datasets without memory overhead
- **๐ŸŽ›๏ธ Dynamic Parallelism**: CPU-aware parallel processing (auto-scales with cores)
- **๐Ÿ“ฆ Smart Batching**: Size-based strategy selection for optimal performance
- **โšก Inline Optimizations**: AggressiveInlining for hot paths
- **๐Ÿ”„ Zero-Copy Operations**: Minimal allocations in critical paths
- **๐Ÿš€ ValueTask Support**: Reduced heap allocations in hot paths with ValueTask
- **๐Ÿ“Š Memory & Span**: Zero-allocation serialization with Memory buffers
- **๐Ÿ”— Pipeline Batching**: ExecuteBatchAsync for multiple operations in single round-trip

## ๐Ÿ“ฆ Installation

Install the package via NuGet:

```bash
dotnet add package RedisKit
```

## ๐ŸŽฏ Quick Start

### Minimal Setup

```csharp
using RedisKit.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Add Redis services with minimal configuration
builder.Services.AddRedisServices(options =>
{
options.ConnectionString = "localhost:6379";
});

var app = builder.Build();

// Use in your controllers or services
app.MapGet("/cache/{key}", async (string key, IRedisCacheService cache) =>
{
var value = await cache.GetAsync(key);
return value ?? "Not found";
});

app.Run();
```

## ๐Ÿ‘ถ Getting Started - Hello Redis!

### Your First Redis Cache

```csharp
using RedisKit.Extensions;
using RedisKit.Interfaces;

// 1. Setup - Add to your Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRedisServices(options =>
{
options.ConnectionString = "localhost:6379";
});

var app = builder.Build();

// 2. Simple String Cache
app.MapPost("/hello/{name}", async (string name, IRedisCacheService cache) =>
{
// Store a simple string
await cache.SetAsync($"greeting:{name}", $"Hello, {name}!", TimeSpan.FromMinutes(5));
return $"Greeting saved for {name}";
});

app.MapGet("/hello/{name}", async (string name, IRedisCacheService cache) =>
{
// Retrieve the string
var greeting = await cache.GetAsync($"greeting:{name}");
return greeting ?? "No greeting found";
});

app.Run();
```

### Counter Example - Increment Values

```csharp
public class CounterService
{
private readonly IRedisCacheService _cache;

public CounterService(IRedisCacheService cache)
{
_cache = cache;
}

public async Task IncrementVisitCountAsync(string page)
{
var key = $"visits:{page}";

// Get current count
var currentCount = await _cache.GetAsync(key) ?? 0;

// Increment and save
currentCount++;
await _cache.SetAsync(key, currentCount, TimeSpan.FromDays(30));

return currentCount;
}
}
```

### Simple User Session

```csharp
public class SessionService
{
private readonly IRedisCacheService _cache;

public SessionService(IRedisCacheService cache)
{
_cache = cache;
}

// Store user session
public async Task CreateSessionAsync(string sessionId, string userId, string userName)
{
var session = new UserSession
{
UserId = userId,
UserName = userName,
LoginTime = DateTime.UtcNow
};

// Session expires in 20 minutes
await _cache.SetAsync($"session:{sessionId}", session, TimeSpan.FromMinutes(20));
}

// Get user session
public async Task GetSessionAsync(string sessionId)
{
return await _cache.GetAsync($"session:{sessionId}");
}

// Extend session
public async Task ExtendSessionAsync(string sessionId)
{
var session = await GetSessionAsync(sessionId);
if (session != null)
{
// Reset expiration to 20 minutes
await _cache.ExpireAsync($"session:{sessionId}", TimeSpan.FromMinutes(20));
}
}
}

public class UserSession
{
public string UserId { get; set; }
public string UserName { get; set; }
public DateTime LoginTime { get; set; }
}
```

## ๐Ÿ”ง Configuration

### Basic Configuration

```csharp
services.AddRedisServices(options =>
{
options.ConnectionString = "localhost:6379";
options.DefaultTtl = TimeSpan.FromHours(1);
options.CacheKeyPrefix = "myapp:";
options.Serializer = SerializerType.MessagePack; // or JSON
});
```

### Redis Sentinel Configuration (High Availability)

```csharp
services.AddRedisServices(options =>
{
// Configure Redis Sentinel for high availability and automatic failover
options.Sentinel = new SentinelOptions
{
Endpoints = new List
{
"sentinel1:26379",
"sentinel2:26379",
"sentinel3:26379"
},
ServiceName = "mymaster",
RedisPassword = "your_redis_password",
EnableFailoverHandling = true,
HealthCheckInterval = TimeSpan.FromSeconds(30)
};

// Note: When using Sentinel, ConnectionString is not required
// RedisKit will automatically discover the master via Sentinel
});
```

### Advanced Configuration

```csharp
services.AddRedisServices(options =>
{
options.ConnectionString = "localhost:6379";
options.DefaultTtl = TimeSpan.FromHours(1);
options.CacheKeyPrefix = "myapp:";

// Retry Configuration
options.RetryConfiguration = new RetryConfiguration
{
MaxAttempts = 3,
Strategy = BackoffStrategy.ExponentialWithJitter,
InitialDelay = TimeSpan.FromSeconds(1),
MaxDelay = TimeSpan.FromSeconds(30),
JitterFactor = 0.2 // 20% jitter
};

// Circuit Breaker
options.CircuitBreaker = new CircuitBreakerSettings
{
Enabled = true,
FailureThreshold = 5,
BreakDuration = TimeSpan.FromSeconds(30),
SuccessThreshold = 2
};

// Health Monitoring
options.HealthMonitoring = new HealthMonitoringSettings
{
Enabled = true,
CheckInterval = TimeSpan.FromSeconds(30),
AutoReconnect = true,
ConsecutiveFailuresThreshold = 3
};

// Connection Timeouts
options.TimeoutSettings = new ConnectionTimeoutSettings
{
ConnectTimeout = TimeSpan.FromSeconds(5),
SyncTimeout = TimeSpan.FromSeconds(5),
AsyncTimeout = TimeSpan.FromSeconds(5),
KeepAlive = TimeSpan.FromSeconds(60)
};
});
```

## ๐Ÿ“š Basic Usage Examples

### Simple Caching with ValueTask (Performance Optimized)

```csharp
public class ProductService
{
private readonly IRedisCacheService _cache;

public ProductService(IRedisCacheService cache)
{
_cache = cache;
}

public async ValueTask GetProductAsync(int productId)
{
var cacheKey = $"product:{productId}";

// Try to get from cache - ValueTask for hot path optimization
var cached = await _cache.GetAsync(cacheKey);
if (cached != null)
return cached;

// Load from database
var product = await LoadFromDatabaseAsync(productId);

// Cache for 1 hour - ValueTask for reduced allocations
if (product != null)
{
await _cache.SetAsync(cacheKey, product, TimeSpan.FromHours(1));
}

return product;
}

public async Task InvalidateProductAsync(int productId)
{
await _cache.DeleteAsync($"product:{productId}");
}
}
```

### High-Performance Batch Operations

```csharp
public class CartService
{
private readonly IRedisCacheService _cache;

public CartService(IRedisCacheService cache)
{
_cache = cache;
}

// ExecuteBatchAsync - Multiple operations in single round-trip
public async Task GetCartSummaryAsync(string userId)
{
var result = await _cache.ExecuteBatchAsync(batch =>
{
batch.GetAsync($"cart:{userId}");
batch.GetAsync($"prefs:{userId}");
batch.GetAsync($"discount:{userId}");
batch.ExistsAsync($"premium:{userId}");
});

return new CartSummary
{
Cart = result.GetResult(0),
Preferences = result.GetResult(1),
DiscountRate = result.GetResult(2) ?? 0,
IsPremium = result.GetResult(3)
};
}
}
```

### Basic Pub/Sub

```csharp
public class NotificationService
{
private readonly IRedisPubSubService _pubSub;
private readonly ILogger _logger;

public NotificationService(IRedisPubSubService pubSub, ILogger logger)
{
_pubSub = pubSub;
_logger = logger;
}

// Publisher
public async Task SendNotificationAsync(string userId, string message)
{
var notification = new UserNotification
{
UserId = userId,
Message = message,
Timestamp = DateTime.UtcNow
};

await _pubSub.PublishAsync($"notifications:{userId}", notification);
}

// Subscriber
public async Task StartListeningAsync(string userId)
{
await _pubSub.SubscribeAsync(
$"notifications:{userId}",
async (notification, ct) =>
{
_logger.LogInformation("Received notification for user {UserId}: {Message}",
notification.UserId, notification.Message);

// Process notification
await ProcessNotificationAsync(notification);
});
}
}

public class UserNotification
{
public string UserId { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public DateTime Timestamp { get; set; }
}
```

### Batch Operations

```csharp
public class BulkOperationService
{
private readonly IRedisCacheService _cache;

public BulkOperationService(IRedisCacheService cache)
{
_cache = cache;
}

public async Task> GetProductsAsync(int[] productIds)
{
// Generate cache keys
var keys = productIds.Select(id => $"product:{id}");

// Get all products in one operation
var cached = await _cache.GetManyAsync(keys);

var result = new Dictionary();
var missingIds = new List();

// Check what we found in cache
foreach (var productId in productIds)
{
var key = $"product:{productId}";
if (cached.TryGetValue(key, out var product) && product != null)
{
result[productId] = product;
}
else
{
missingIds.Add(productId);
}
}

// Load missing from database
if (missingIds.Any())
{
var products = await LoadProductsFromDatabaseAsync(missingIds);

// Cache them
var toCache = new Dictionary();
foreach (var product in products)
{
result[product.Id] = product;
toCache[$"product:{product.Id}"] = product;
}

await _cache.SetManyAsync(toCache, TimeSpan.FromHours(1));
}

return result;
}
}
```

## ๐Ÿš€ Advanced Usage Examples

### Pattern-Based Subscriptions

```csharp
public class GameEventService
{
private readonly IRedisPubSubService _pubSub;
private readonly ILogger _logger;
private SubscriptionToken? _token;

public GameEventService(IRedisPubSubService pubSub, ILogger logger)
{
_pubSub = pubSub;
_logger = logger;
}

public async Task StartMonitoringAsync()
{
// Subscribe to all game events using pattern
_token = await _pubSub.SubscribePatternAsync(
"game:*:events",
async (gameEvent, ct) =>
{
_logger.LogInformation("Game {GameId} - Event: {EventType}",
gameEvent.GameId, gameEvent.EventType);

switch (gameEvent.EventType)
{
case "player_joined":
await HandlePlayerJoinedAsync(gameEvent);
break;
case "game_started":
await HandleGameStartedAsync(gameEvent);
break;
case "game_ended":
await HandleGameEndedAsync(gameEvent);
break;
}
});

// You can also subscribe with channel metadata
await _pubSub.SubscribePatternWithChannelAsync(
"game:*:critical",
async (gameEvent, channel, ct) =>
{
// Extract game ID from channel name
var parts = channel.Split(':');
var gameId = parts[1];

_logger.LogCritical("Critical event in game {GameId}: {Message}",
gameId, gameEvent.Message);

await SendAlertAsync(gameId, gameEvent);
});
}

public async Task StopMonitoringAsync()
{
if (_token != null)
{
await _token.UnsubscribeAsync();
}
}
}
```

### Redis Streams with Consumer Groups

```csharp
public class OrderProcessingService : BackgroundService
{
private readonly IRedisStreamService _streams;
private readonly ILogger _logger;

public OrderProcessingService(IRedisStreamService streams, ILogger logger)
{
_streams = streams;
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Create consumer group
await _streams.CreateConsumerGroupAsync("orders", "order-processors");

while (!stoppingToken.IsCancellationRequested)
{
try
{
// Read messages from stream
var messages = await _streams.ReadGroupAsync(
"orders",
"order-processors",
"processor-1",
count: 10,
cancellationToken: stoppingToken);

foreach (var message in messages)
{
try
{
await ProcessOrderAsync(message.Data);

// Acknowledge message
await _streams.AcknowledgeAsync("orders", "order-processors", message.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order {OrderId}", message.Data?.OrderId);
// Message will be retried
}
}

// Process pending messages (retry failed ones)
var retryResult = await _streams.RetryPendingMessagesAsync(
"orders",
"order-processors",
"processor-1",
async (order) =>
{
await ProcessOrderAsync(order);
return true; // Success
},
cancellationToken: stoppingToken);

if (retryResult.FailureCount > 0)
{
_logger.LogWarning("Failed to process {Count} orders, moved to DLQ",
retryResult.DeadLetterCount);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in order processing loop");
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
}

private async Task ProcessOrderAsync(Order? order)
{
if (order == null) return;

_logger.LogInformation("Processing order {OrderId}", order.OrderId);

// Process the order
await Task.Delay(100); // Simulate work

// Publish completion event
await _pubSub.PublishAsync($"orders:{order.OrderId}:completed", new OrderCompleted
{
OrderId = order.OrderId,
CompletedAt = DateTime.UtcNow
});
}
}
```

### Cache-Aside Pattern with Statistics

```csharp
public class CachedRepository where T : class, IEntity
{
private readonly IRedisCacheService _cache;
private readonly ILogger> _logger;
private readonly string _entityName;
private long _hits = 0;
private long _misses = 0;

public CachedRepository(IRedisCacheService cache, ILogger> logger)
{
_cache = cache;
_logger = logger;
_entityName = typeof(T).Name.ToLower();
}

public async Task GetByIdAsync(string id, Func> dataLoader)
{
var key = $"{_entityName}:{id}";

// Try cache first
var cached = await _cache.GetAsync(key);
if (cached != null)
{
Interlocked.Increment(ref _hits);
return cached;
}

Interlocked.Increment(ref _misses);

// Load from source
var entity = await dataLoader();
if (entity != null)
{
// Cache with sliding expiration
await _cache.SetAsync(key, entity, TimeSpan.FromMinutes(15));
}

return entity;
}

public async Task GetOrCreateAsync(string id, Func> factory)
{
var key = $"{_entityName}:{id}";

var cached = await _cache.GetAsync(key);
if (cached != null)
{
Interlocked.Increment(ref _hits);
return cached;
}

Interlocked.Increment(ref _misses);

// Use distributed lock to prevent cache stampede
var lockKey = $"lock:{key}";
var lockAcquired = await _cache.SetAsync(
lockKey,
"locked",
TimeSpan.FromSeconds(30),
when: When.NotExists);

if (lockAcquired)
{
try
{
// Double-check after acquiring lock
cached = await _cache.GetAsync(key);
if (cached != null)
return cached;

// Create new entity
var entity = await factory();
await _cache.SetAsync(key, entity, TimeSpan.FromMinutes(15));
return entity;
}
finally
{
await _cache.DeleteAsync(lockKey);
}
}
else
{
// Wait for other thread to populate cache
await Task.Delay(100);
return await GetByIdAsync(id, factory) ?? await factory();
}
}

public CacheStatistics GetStatistics()
{
var total = _hits + _misses;
return new CacheStatistics
{
Hits = _hits,
Misses = _misses,
HitRate = total > 0 ? (double)_hits / total : 0
};
}
}

public interface IEntity
{
string Id { get; }
}

public class CacheStatistics
{
public long Hits { get; set; }
public long Misses { get; set; }
public double HitRate { get; set; }
}
```

## ๐ŸŽจ Custom Serializer Implementation

### Creating a Custom Serializer

```csharp
using RedisKit.Serialization;
using ProtoBuf;

// Custom Protobuf serializer
public class ProtobufRedisSerializer : IRedisSerializer
{
public string Name => "Protobuf";

public Task SerializeAsync(T value, CancellationToken cancellationToken = default)
where T : class
{
if (value == null)
return Task.FromResult(Array.Empty());

using var stream = new MemoryStream();
Serializer.Serialize(stream, value);
return Task.FromResult(stream.ToArray());
}

public Task DeserializeAsync(byte[] data, CancellationToken cancellationToken = default)
where T : class
{
if (data == null || data.Length == 0)
return Task.FromResult(null);

using var stream = new MemoryStream(data);
var result = Serializer.Deserialize(stream);
return Task.FromResult(result);
}

public Task DeserializeAsync(byte[] data, Type type, CancellationToken cancellationToken = default)
{
if (data == null || data.Length == 0)
return Task.FromResult(null);

using var stream = new MemoryStream(data);
var result = Serializer.Deserialize(type, stream);
return Task.FromResult(result);
}
}

// Register custom serializer
public class CustomSerializerFactory : IRedisSerializerFactory
{
public IRedisSerializer Create(SerializerType type)
{
return type switch
{
SerializerType.Custom => new ProtobufRedisSerializer(),
_ => RedisSerializerFactory.Create(type)
};
}
}
```

### Using Custom Serializer

```csharp
// Option 1: Register globally
services.AddRedisServices(options =>
{
options.ConnectionString = "localhost:6379";
options.Serializer = SerializerType.Custom;
options.CustomSerializerFactory = new CustomSerializerFactory();
});

// Option 2: Use for specific service
services.AddSingleton();
services.AddSingleton(provider =>
{
var database = provider.GetRequiredService();
var logger = provider.GetRequiredService>();
var options = provider.GetRequiredService>();
var serializer = provider.GetRequiredService();

return new RedisCacheService(database, logger, options.Value, serializer);
});
```

### Compression Serializer Wrapper

```csharp
using System.IO.Compression;

public class CompressedSerializer : IRedisSerializer
{
private readonly IRedisSerializer _innerSerializer;
private readonly CompressionLevel _compressionLevel;

public CompressedSerializer(
IRedisSerializer innerSerializer,
CompressionLevel compressionLevel = CompressionLevel.Optimal)
{
_innerSerializer = innerSerializer;
_compressionLevel = compressionLevel;
}

public string Name => $"Compressed_{_innerSerializer.Name}";

public async Task SerializeAsync(T value, CancellationToken cancellationToken = default)
where T : class
{
var data = await _innerSerializer.SerializeAsync(value, cancellationToken);

using var output = new MemoryStream();
using (var gzip = new GZipStream(output, _compressionLevel))
{
await gzip.WriteAsync(data, 0, data.Length, cancellationToken);
}

return output.ToArray();
}

public async Task DeserializeAsync(byte[] data, CancellationToken cancellationToken = default)
where T : class
{
using var input = new MemoryStream(data);
using var gzip = new GZipStream(input, CompressionMode.Decompress);
using var output = new MemoryStream();

await gzip.CopyToAsync(output, cancellationToken);
var decompressed = output.ToArray();

return await _innerSerializer.DeserializeAsync(decompressed, cancellationToken);
}

public async Task DeserializeAsync(
byte[] data,
Type type,
CancellationToken cancellationToken = default)
{
using var input = new MemoryStream(data);
using var gzip = new GZipStream(input, CompressionMode.Decompress);
using var output = new MemoryStream();

await gzip.CopyToAsync(output, cancellationToken);
var decompressed = output.ToArray();

return await _innerSerializer.DeserializeAsync(decompressed, type, cancellationToken);
}
}

// Usage
services.AddSingleton(provider =>
{
var innerSerializer = RedisSerializerFactory.Create(SerializerType.MessagePack);
return new CompressedSerializer(innerSerializer, CompressionLevel.Fastest);
});
```

## ๐Ÿ—๏ธ Dependency Injection

```csharp
// In your Program.cs or Startup.cs:
var builder = WebApplication.CreateBuilder(args);

// Add Redis services with configuration from appsettings.json
builder.Services.Configure(
builder.Configuration.GetSection("Redis"));

builder.Services.AddRedisServices(options =>
{
builder.Configuration.GetSection("Redis").Bind(options);
});

// In your services:
public class UserService
{
private readonly IRedisCacheService _cache;
private readonly IRedisPubSubService _pubSub;
private readonly IRedisStreamService _stream;
private readonly ILogger _logger;

public UserService(
IRedisCacheService cache,
IRedisPubSubService pubSub,
IRedisStreamService stream,
ILogger logger)
{
_cache = cache;
_pubSub = pubSub;
_stream = stream;
_logger = logger;
}

public async Task GetUserAsync(string userId)
{
// Try cache first
var cached = await _cache.GetAsync($"user:{userId}");
if (cached != null)
return cached;

// Load from database
var user = await LoadFromDatabaseAsync(userId);

// Cache for future requests
if (user != null)
{
await _cache.SetAsync($"user:{userId}", user, TimeSpan.FromHours(1));

// Publish update event
await _pubSub.PublishAsync("user-updates", new UserLoadedEvent
{
UserId = userId
});
}

return user;
}
}
```

## ๐ŸŽฏ Backoff Strategies

RedisKit supports multiple backoff strategies for retry operations:

- **Fixed**: Constant delay between retries
- **Linear**: Linear increase in delay
- **Exponential**: Exponential increase in delay
- **ExponentialWithJitter**: Exponential with random jitter to prevent thundering herd
- **DecorrelatedJitter**: AWS-recommended strategy with decorrelated jitter

```csharp
options.RetryConfiguration = new RetryConfiguration
{
Strategy = BackoffStrategy.DecorrelatedJitter,
MaxAttempts = 5,
InitialDelay = TimeSpan.FromMilliseconds(100),
MaxDelay = TimeSpan.FromSeconds(10)
};
```

## ๐Ÿš€ Performance Tips & Best Practices

### 1. **Connection Management**

```csharp
// โŒ DON'T: Create new connections for each operation
public async Task BadExample()
{
var connection = await ConnectionMultiplexer.ConnectAsync("localhost");
var db = connection.GetDatabase();
await db.StringSetAsync("key", "value");
connection.Dispose(); // Connection closed!
}

// โœ… DO: Use dependency injection and connection pooling
public class GoodExample
{
private readonly IRedisCacheService _cache; // Injected, pooled connection

public async Task SetValueAsync()
{
await _cache.SetAsync("key", "value");
}
}
```

### 2. **Batch Operations for Better Performance**

```csharp
// โŒ DON'T: Multiple round trips
public async Task SlowApproach(string[] userIds)
{
var users = new List();
foreach (var id in userIds)
{
var user = await _cache.GetAsync($"user:{id}");
if (user != null) users.Add(user);
}
}

// โœ… DO: Single batch operation
public async Task FastApproach(string[] userIds)
{
var keys = userIds.Select(id => $"user:{id}");
var results = await _cache.GetManyAsync(keys);
var users = results.Values.Where(u => u != null).ToList();
}
```

### 3. **Big Key Handling**

```csharp
// โŒ DON'T: Store huge objects as single keys
public async Task BadBigKey()
{
var hugeList = new List(1_000_000); // 1 million items!
await _cache.SetAsync("huge:list", hugeList); // This blocks Redis!
}

// โœ… DO: Split large datasets
public async Task GoodBigKeyHandling()
{
var items = GetLargeDataset();
var chunks = items.Chunk(1000); // Split into chunks of 1000

var tasks = chunks.Select((chunk, index) =>
_cache.SetAsync($"items:chunk:{index}", chunk.ToList(), TimeSpan.FromHours(1))
);

await Task.WhenAll(tasks);
}

// โœ… DO: Use Redis Streams for large datasets
public async Task StreamApproach(List items)
{
foreach (var batch in items.Chunk(100))
{
foreach (var item in batch)
{
await _streamService.AddAsync("items:stream", item);
}
}
}
```

### 4. **Pipeline Usage**

```csharp
// โŒ DON'T: Sequential operations
public async Task SlowSequential()
{
await _cache.SetAsync("key1", "value1");
await _cache.SetAsync("key2", "value2");
await _cache.SetAsync("key3", "value3");
// 3 round trips to Redis
}

// โœ… DO: Use batch/pipeline operations
public async Task FastPipeline()
{
var items = new Dictionary
{
["key1"] = "value1",
["key2"] = "value2",
["key3"] = "value3"
};

await _cache.SetManyAsync(items, TimeSpan.FromHours(1));
// Single round trip!
}
```

### 5. **Memory Optimization**

```csharp
// โœ… Use appropriate serializers
services.AddRedisServices(options =>
{
// MessagePack: Fastest and smallest
options.Serializer = SerializerType.MessagePack;

// JSON: Human readable, larger size
// options.Serializer = SerializerType.SystemTextJson;
});

// โœ… Compress large objects
public class CompressedCacheService
{
private readonly IRedisCacheService _cache;

public async Task SetCompressedAsync(string key, T value) where T : class
{
if (value is string str && str.Length > 1000)
{
// Compress strings larger than 1KB
var compressed = Compress(str);
await _cache.SetAsync($"{key}:compressed", compressed);
}
else
{
await _cache.SetAsync(key, value);
}
}
}
```

### 6. **Key Expiration Strategies**

```csharp
// โœ… Use sliding expiration for frequently accessed data
public async Task GetWithSlidingExpirationAsync(string key) where T : class
{
var value = await _cache.GetAsync(key);
if (value != null)
{
// Reset expiration on each access
await _cache.ExpireAsync(key, TimeSpan.FromMinutes(30));
}
return value;
}

// โœ… Use absolute expiration for time-sensitive data
public async Task SetDailyReportAsync(Report report)
{
var tomorrow = DateTime.UtcNow.Date.AddDays(1);
var ttl = tomorrow - DateTime.UtcNow;

await _cache.SetAsync($"report:{DateTime.UtcNow:yyyy-MM-dd}", report, ttl);
}
```

### 7. **Avoid Hot Keys**

```csharp
// โŒ DON'T: Single key for global counter
public async Task IncrementGlobalCounter()
{
var count = await _cache.GetAsync("global:counter");
await _cache.SetAsync("global:counter", count + 1);
// This key becomes a bottleneck!
}

// โœ… DO: Distribute load across multiple keys
public async Task IncrementDistributedCounter()
{
var shard = Random.Shared.Next(0, 10); // 10 shards
var key = $"counter:shard:{shard}";

var count = await _cache.GetAsync(key);
await _cache.SetAsync(key, count + 1);
}

public async Task GetTotalCount()
{
var tasks = Enumerable.Range(0, 10)
.Select(i => _cache.GetAsync($"counter:shard:{i}"));

var counts = await Task.WhenAll(tasks);
return counts.Sum();
}
```

### 8. **Circuit Breaker for Resilience**

```csharp
// โœ… Configure circuit breaker to prevent cascade failures
services.AddRedisServices(options =>
{
options.CircuitBreaker = new CircuitBreakerSettings
{
Enabled = true,
FailureThreshold = 5, // Open after 5 failures
BreakDuration = TimeSpan.FromSeconds(30), // Stay open for 30s
SuccessThreshold = 2 // Need 2 successes to close
};
});
```

### 9. **Monitoring & Metrics**

```csharp
// โœ… Track cache hit rates
public class MetricsCacheService
{
private readonly IRedisCacheService _cache;
private readonly IMetrics _metrics;

public async Task GetWithMetricsAsync(string key) where T : class
{
var value = await _cache.GetAsync(key);

if (value != null)
_metrics.Increment("cache.hits");
else
_metrics.Increment("cache.misses");

return value;
}
}
```

### 10. **Pub/Sub Performance**

```csharp
// โœ… Use pattern subscriptions wisely
public class EfficientPubSub
{
private readonly IRedisPubSubService _pubSub;

public async Task SubscribeEfficiently()
{
// Instead of subscribing to many individual channels
// Use pattern subscription
await _pubSub.SubscribePatternAsync(
"events:*", // Single pattern subscription
async (evt, ct) => await ProcessEventAsync(evt, ct)
);
}
}
```

## โšก Recent Performance Improvements

### SetManyAsync Optimization with Lua Scripts

We've implemented a significant performance optimization for batch operations using Lua scripts:

- **90-95% performance improvement** for batch SET operations
- **Single round-trip** to Redis instead of O(n) operations
- **Automatic fallback** for environments without Lua support
- **Parallel serialization** for optimal CPU utilization

#### Before vs After Performance

| Batch Size | Before (ms) | After (ms) | Improvement |
|------------|-------------|------------|----------------|
| 100 items | 52 | 3 | **94% faster** |
| 500 items | 258 | 14 | **95% faster** |
| 1000 items | 516 | 28 | **95% faster** |
| 5000 items | 2,580 | 140 | **95% faster** |

*Benchmarks on local Redis with 1KB objects*

## ๐Ÿ“Š Performance Benchmarks

### Serializer Performance Comparison

| Method | JSON (ns) | MessagePack (ns) | Speed Improvement | Memory Improvement |
|---------------------------------|-----------|------------------|-------------------|----------------------|
| **Small Object Serialize** | 331.8 | 143.2 | **2.3x faster** | **5.6x less memory** |
| **Large Object Serialize** | 3,569.1 | 1,940.7 | **1.8x faster** | **Similar memory** |
| **Array Serialize (100 items)** | 28,143.8 | 11,556.8 | **2.4x faster** | **3.2x less memory** |
| **Small Object Deserialize** | 628.0 | 256.5 | **2.4x faster** | **2.1x less memory** |
| **Async Serialize** | 355.9 | 173.8 | **2.0x faster** | **2.8x less memory** |
| **Async Deserialize** | 823.8 | 290.0 | **2.8x faster** | **2.0x less memory** |

> **Recommendation**: Use MessagePack for production workloads requiring high performance and low memory usage.

### Redis Operations Performance

| Operation | Single Item | Batch (100 items) | Batch (1000 items) |
|------------|-------------|-------------------|--------------------|
| Set | ~1ms | ~5ms | ~40ms |
| Get | ~0.8ms | ~4ms | ~35ms |
| Pub/Sub | ~0.5ms | N/A | N/A |
| Stream Add | ~1.2ms | ~8ms | ~70ms |

*Benchmarks on local Redis, actual performance depends on network latency and Redis server specs*

## ๐Ÿ“Š Performance Considerations

- **Connection Pooling**: Connections are automatically pooled and reused
- **Pipelining**: Commands are automatically pipelined for better throughput
- **Memory Efficiency**: Uses ArrayPool and MemoryPool to minimize allocations
- **Concurrent Operations**: Thread-safe operations with minimal locking
- **Circuit Breaker**: Prevents cascading failures in distributed systems
- **Automatic Cleanup**: Inactive handlers are automatically cleaned up to prevent memory leaks

## ๐Ÿงช Testing

The library includes comprehensive unit tests with 100% coverage of critical paths:

```bash
dotnet test

# Results
Passed! - Failed: 0, Passed: 140, Skipped: 12, Total: 152
```

## ๐Ÿ“‹ Requirements

- .NET 9.0 or higher
- Redis Server 5.0 or higher (6.0+ recommended for Streams support)

## ๐Ÿ”’ Security & Dependencies

- **Automated Dependency Updates**: Dependabot configured for weekly security updates
- **Security Policy**: See [SECURITY.md](SECURITY.md) for vulnerability reporting
- **CI/CD Pipeline**: Automated testing on multiple platforms (Windows, Linux, macOS)
- **Auto-merge**: Minor and patch updates are automatically merged after passing tests

## ๐Ÿค Contributing

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

## ๐Ÿ“„ License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## ๐Ÿ“š Documentation

For more detailed documentation, please visit our [Wiki](https://github.com/ersintarhan/RedisKit/wiki).

## ๐Ÿ†• Redis 7.x Usage Examples

### Redis Functions

```csharp
// Create and load a function library
var functionService = app.Services.GetRequiredService();

// Build a function library with FunctionLibraryBuilder
var library = new FunctionLibraryBuilder()
.WithName("mylib")
.WithEngine("LUA")
.WithDescription("My custom functions")
.AddFunction("greet", @"
function(keys, args)
return 'Hello, ' .. args[1]
end
")
.AddFunction("sum", @"
function(keys, args)
return tonumber(args[1]) + tonumber(args[2])
end
")
.Build();

// Load the library
await functionService.LoadAsync(library);

// Call functions
var greeting = await functionService.CallAsync("greet", args: new[] { "World" });
// Result: "Hello, World"

var sum = await functionService.CallAsync("sum", args: new[] { "10", "20" });
// Result: 30

// Call with array return type
var results = await functionService.CallAsync("get_users");
// Result: ["user1", "user2", "user3"]
```

### Sharded Pub/Sub

```csharp
// Sharded Pub/Sub for better scalability in cluster mode
var shardedPubSub = app.Services.GetRequiredService();

// Subscribe to a sharded channel (distributed across shards)
var token = await shardedPubSub.SubscribeAsync(
"orders:new",
async (message, ct) =>
{
Console.WriteLine($"Order received on shard: {message.ShardId}");
await ProcessOrder(message.Data);
});

// Publish to sharded channel (automatically routed to correct shard)
var subscribers = await shardedPubSub.PublishAsync(
"orders:new",
new OrderMessage { OrderId = 123, Amount = 99.99m });

// Note: Pattern subscriptions are NOT supported in sharded pub/sub
// Use regular pub/sub for patterns
```

## ๐Ÿ› Known Issues

- Stream service tests are currently skipped as they require a real Redis instance
- PUBSUB NUMSUB command returns local handler count only (StackExchange.Redis limitation)
- Sharded Pub/Sub does not support pattern subscriptions (Redis limitation)

## ๐Ÿšฆ Roadmap

- [x] Redis Functions support (Redis 7.x) - โœ… Completed
- [x] Sharded Pub/Sub (Redis 7.x) - โœ… Completed
- [x] Distributed locking primitives - โœ… Completed
- [x] Redis Sentinel support (High Availability with automatic failover)
- [ ] Redis Cluster native support
- [ ] ACL v2 improvements (Redis 7.x)
- [ ] Client-side caching support
- [ ] Geo-spatial operations
- [ ] Time-series data support
- [ ] OpenTelemetry integration
- [ ] Prometheus metrics export