StackExchange.Redis.Extensions.AspNetCore
13.0.1
Prefix Reserved
dotnet add package StackExchange.Redis.Extensions.AspNetCore --version 13.0.1
NuGet\Install-Package StackExchange.Redis.Extensions.AspNetCore -Version 13.0.1
<PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="13.0.1" />
<PackageVersion Include="StackExchange.Redis.Extensions.AspNetCore" Version="13.0.1" />
<PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" />
paket add StackExchange.Redis.Extensions.AspNetCore --version 13.0.1
#r "nuget: StackExchange.Redis.Extensions.AspNetCore, 13.0.1"
#:package StackExchange.Redis.Extensions.AspNetCore@13.0.1
#addin nuget:?package=StackExchange.Redis.Extensions.AspNetCore&version=13.0.1
#tool nuget:?package=StackExchange.Redis.Extensions.AspNetCore&version=13.0.1
StackExchange.Redis.Extensions
StackExchange.Redis.Extensions is a library that extends StackExchange.Redis, making it easier to work with Redis in .NET applications. It wraps the base library with serialization, connection pooling, and higher-level APIs so you can store and retrieve complex objects without writing boilerplate code.
AI-Ready: This library provides an
llms.txtfile for AI coding assistants and a Claude Code plugin for configuration, scaffolding, and troubleshooting.claude plugin add imperugo/StackExchange.Redis.ExtensionsThen use
/redis-configure,/redis-scaffold, or/redis-diagnosein Claude Code.
Features
- Store and retrieve complex .NET objects with automatic serialization
- Multiple serialization providers (System.Text.Json, Newtonsoft, Protobuf, MsgPack, MemoryPack, and more)
- Connection pooling with LeastLoaded and RoundRobin strategies
- Pub/Sub messaging with typed handlers
- Hash operations with per-field expiry (Redis 7.4+)
- GeoSpatial indexes (GEOADD, GEOSEARCH, GEODIST, etc.)
- Redis Streams with consumer group support
- Set, List, and Sorted Set operations with Set Combine (union, intersect, diff)
- HyperLogLog for probabilistic cardinality estimation
- Distributed locking with IAsyncDisposable auto-release
- Bitmap operations for analytics and feature flags
- Lua scripting for atomic server-side operations
- Key tagging and search
- Key management (rename, type, dump/restore)
- Atomic counters (StringIncrement/StringDecrement)
- Transparent compression (GZip, Brotli, LZ4, Snappy, Zstandard)
IDistributedCacheadapter (Microsoft-compatible Hash schema)- ASP.NET Core Health Check for connection pool monitoring
- Azure Managed Identity support
- ASP.NET Core integration with DI and .NET 8+ Keyed Services
- Multiple named Redis instances with
[FromKeyedServices]support - OpenTelemetry integration
- .NET Standard 2.1, .NET 8, .NET 9, .NET 10
Architecture
graph TB
App[Your Application] --> DI[ASP.NET Core DI]
DI --> Factory[IRedisClientFactory]
Factory --> Client1[IRedisClient - Default]
Factory --> Client2[IRedisClient - Named]
Client1 --> DB[IRedisDatabase]
DB --> Pool[Connection Pool Manager]
Pool --> C1[Connection 1]
Pool --> C2[Connection 2]
Pool --> C3[Connection N...]
DB --> Ser[ISerializer]
Ser --> Comp[CompressedSerializer?]
Comp --> Inner[System.Text.Json / Newtonsoft / ...]
C1 --> Redis[(Redis Server)]
C2 --> Redis
C3 --> Redis
Quick Start
1. Install packages
dotnet add package StackExchange.Redis.Extensions.Core
dotnet add package StackExchange.Redis.Extensions.System.Text.Json
dotnet add package StackExchange.Redis.Extensions.AspNetCore
2. Configure in appsettings.json
{
"Redis": {
"Password": "",
"AllowAdmin": true,
"Ssl": false,
"ConnectTimeout": 5000,
"SyncTimeout": 5000,
"Database": 0,
"Hosts": [
{ "Host": "localhost", "Port": 6379 }
],
"PoolSize": 5,
"IsDefault": true
}
}
3. Register in DI
var redisConfig = builder.Configuration.GetSection("Redis").Get<RedisConfiguration>();
builder.Services.AddStackExchangeRedisExtensions<SystemTextJsonSerializer>(redisConfig);
4. Use it
public class MyService(IRedisDatabase redis)
{
public async Task Example()
{
// Store an object
await redis.AddAsync("user:1", new User { Name = "Ugo", Age = 38 });
// Retrieve it
var user = await redis.GetAsync<User>("user:1");
// Store with expiry
await redis.AddAsync("session:abc", sessionData, TimeSpan.FromMinutes(30));
// Bulk operations
var items = new[]
{
Tuple.Create("key1", "value1"),
Tuple.Create("key2", "value2"),
};
await redis.AddAllAsync(items, TimeSpan.FromHours(1));
// Search keys
var keys = await redis.SearchKeysAsync("user:*");
}
}
NuGet Packages
Core
| Package | Description | NuGet |
|---|---|---|
| Core | Core library with abstractions and implementations | |
| AspNetCore | ASP.NET Core DI integration and middleware |
Serializers (pick one)
Compressors (optional)
Usage Examples
Hash Operations
// Set a hash field
await redis.HashSetAsync("user:1", "name", "Ugo");
await redis.HashSetAsync("user:1", "email", "ugo@example.com");
// Get a hash field
var name = await redis.HashGetAsync<string>("user:1", "name");
// Set with per-field expiry (Redis 7.4+)
await redis.HashSetWithExpiryAsync("user:1", "session", sessionData, TimeSpan.FromMinutes(30));
// Query field TTL
var ttl = await redis.HashFieldGetTimeToLiveAsync("user:1", new[] { "session" });
GeoSpatial
// Add locations
await redis.GeoAddAsync("restaurants", new[]
{
new GeoEntry(13.361389, 38.115556, "Pizzeria Da Michele"),
new GeoEntry(15.087269, 37.502669, "Trattoria del Corso"),
new GeoEntry(12.496366, 41.902782, "Da Enzo al 29"),
});
// Distance between two places
var km = await redis.GeoDistanceAsync("restaurants",
"Pizzeria Da Michele", "Trattoria del Corso", GeoUnit.Kilometers);
// Search within 200km of a point
var nearby = await redis.GeoSearchAsync("restaurants", 13.361389, 38.115556,
new GeoSearchCircle(200, GeoUnit.Kilometers),
count: 10, order: Order.Ascending);
Redis Streams
// Publish typed events
await redis.StreamAddAsync("orders", "payload", new Order { Id = 1, Total = 99.99m });
// Consumer group workflow
await redis.StreamCreateConsumerGroupAsync("orders", "processors");
var entries = await redis.StreamReadGroupAsync("orders", "processors", "worker-1");
foreach (var entry in entries)
{
// Process the message
await redis.StreamAcknowledgeAsync("orders", "processors", entry.Id!);
}
Pub/Sub
// Subscribe to typed messages
await redis.SubscribeAsync<OrderEvent>("orders:new", async order =>
{
Console.WriteLine($"New order: {order.Id}");
});
// Publish
await redis.PublishAsync("orders:new", new OrderEvent { Id = 42 });
Atomic Counters
// Increment/decrement with long (default step = 1)
var views = await redis.StringIncrementAsync("page:views");
var stock = await redis.StringDecrementAsync("product:123:stock");
// Custom step
await redis.StringIncrementAsync("stats:bytes", 1024);
// Double precision
await redis.StringIncrementAsync("account:balance", 49.99);
Set Operations
// Combine sets: union, intersect, difference
var allTags = await redis.SetCombineAsync<string>(SetOperation.Union, "user:1:tags", "user:2:tags");
var commonTags = await redis.SetCombineAsync<string>(SetOperation.Intersect, "user:1:tags", "user:2:tags");
// Store result in a new key
await redis.SetCombineAndStoreAsync(SetOperation.Union, "all:tags", new[] { "set:a", "set:b", "set:c" });
HyperLogLog
// Count unique visitors
await redis.HyperLogLogAddAsync("page:home:visitors", userId);
var uniqueCount = await redis.HyperLogLogLengthAsync("page:home:visitors");
// Merge daily counts into a monthly aggregate
await redis.HyperLogLogMergeAsync("visitors:2024:01", new[]
{
"visitors:2024:01:01",
"visitors:2024:01:02",
"visitors:2024:01:03",
});
Distributed Lock
// Acquire a lock with automatic release on dispose
await using var lockObj = await redis.LockAcquireAsync(
"resource:order:123",
expiry: TimeSpan.FromSeconds(30),
maxRetries: 5,
retryDelay: TimeSpan.FromMilliseconds(200));
if (lockObj is not null)
{
// Critical section — lock is held
await ProcessOrder(123);
}
// Lock released automatically on dispose
Bitmap Operations
// Track daily active users (1 bit per user ID)
await redis.StringSetBitAsync("dau:2024-01-15", userId, true);
// Count active users
var activeCount = await redis.StringBitCountAsync("dau:2024-01-15");
// Compute users active on ALL days (AND operation)
await redis.StringBitOperationAsync(Bitwise.And, "wau:all-days", new[]
{
"dau:2024-01-15", "dau:2024-01-16", "dau:2024-01-17",
});
Lua Scripting
// Atomic server-side operations
var script = @"
local current = tonumber(redis.call('GET', KEYS[1]) or '0')
if current < tonumber(ARGV[1]) then
return redis.call('INCR', KEYS[1])
end
return current";
var result = await redis.ScriptEvaluateAsync(
script,
new RedisKey[] { "counter:views" },
new RedisValue[] { 100 });
Key Management
// Rename a key
await redis.KeyRenameAsync("old-key", "new-key");
// Check key type
var type = await redis.KeyTypeAsync("my-key"); // RedisType.String, Set, Hash, ...
// Dump and restore (migrate between databases)
var dump = await redis.KeyDumpAsync("my-key");
await redis.KeyRestoreAsync("my-key-copy", dump, TimeSpan.FromHours(1));
IDistributedCache
// Register the IDistributedCache adapter (call after AddStackExchangeRedisExtensions)
builder.Services.AddStackExchangeRedisExtensions<SystemTextJsonSerializer>(redisConfig);
builder.Services.AddRedisDistributedCache();
// Use standard IDistributedCache anywhere
public class MyService(IDistributedCache cache)
{
public async Task CacheData()
{
await cache.SetAsync("session:abc", data, new DistributedCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(20),
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(4),
});
var cached = await cache.GetAsync("session:abc");
}
}
Note: Uses a Hash-based schema (
data/absexp/sldexp) compatible withMicrosoft.Extensions.Caching.StackExchangeRedis, enabling zero-downtime migration between providers.
Health Check
// Register the health check
builder.Services.AddHealthChecks()
.AddRedisExtensionsHealthCheck();
// Returns:
// - Healthy: all pool connections active + PING OK
// - Degraded: some connections invalid but Redis responding
// - Unhealthy: all connections invalid or PING fails
Keyed DI Services (.NET 8+)
// Register with named configurations
builder.Services.AddStackExchangeRedisExtensions<SystemTextJsonSerializer>(new[]
{
new RedisConfiguration { Name = "cache", IsDefault = true, /* ... */ },
new RedisConfiguration { Name = "session", /* ... */ },
});
// Inject by name using [FromKeyedServices]
public class MyService(
[FromKeyedServices("cache")] IRedisDatabase cacheDb,
[FromKeyedServices("session")] IRedisDatabase sessionDb)
{
// Each resolves to its own Redis instance with isolated connection pool
}
Compression
// Enable transparent compression with any serializer
services.AddStackExchangeRedisExtensions<SystemTextJsonSerializer>(config);
services.AddRedisCompression<LZ4Compressor>(); // That's it!
// All operations automatically compress/decompress
await redis.AddAsync("large-data", myLargeObject); // stored compressed
var obj = await redis.GetAsync<MyObject>("large-data"); // decompressed automatically
Azure Managed Identity
var config = new RedisConfiguration { /* ... */ };
config.ConfigurationOptionsAsyncHandler = async opts =>
{
await opts.ConfigureForAzureWithTokenCredentialAsync(new DefaultAzureCredential());
return opts;
};
Connection Pooling
graph LR
subgraph Pool["Connection Pool (PoolSize=5)"]
C1["Connection 1<br/>Outstanding: 3"]
C2["Connection 2<br/>Outstanding: 0"]
C3["Connection 3<br/>Outstanding: 7"]
C4["Connection 4<br/>Outstanding: 1"]
C5["Connection 5<br/>Outstanding: 2"]
end
Op["GetConnection()"] -->|LeastLoaded| C2
Op -->|RoundRobin| C4
style C2 fill:#90EE90
style C4 fill:#87CEEB
The pool automatically skips disconnected connections and falls back gracefully when all connections are down, letting StackExchange.Redis's internal reconnection logic recover.
| Strategy | Behavior |
|---|---|
LeastLoaded (default) |
Picks the connected connection with fewest outstanding commands |
RoundRobin |
Random selection among connected connections |
Serialization Behavior
All values stored in Redis go through the configured ISerializer. This means:
- A
stringvalue"hello"is stored as"\"hello\""(JSON-encoded) - Use
IRedisDatabase.Databasefor raw Redis operations without serialization - All serializers follow the same convention:
nullinput produces an empty byte array
Configuration Reference
| Property | Default | Description |
|---|---|---|
Hosts |
Required | Redis server endpoints |
Password |
null |
Redis password |
Database |
0 |
Database index |
Ssl |
false |
Enable TLS |
PoolSize |
5 |
Number of connections in the pool |
ConnectionSelectionStrategy |
LeastLoaded |
Pool selection strategy |
SyncTimeout |
5000 |
Sync operation timeout (ms) |
ConnectTimeout |
5000 |
Connection timeout (ms) |
KeyPrefix |
"" |
Prefix for all keys and channels |
AllowAdmin |
false |
Enable admin commands |
ClientName |
null |
Connection client name |
KeepAlive |
-1 |
Heartbeat interval (seconds). -1 = SE.Redis default, 0 = disabled |
ServiceName |
null |
Sentinel service name |
MaxValueLength |
0 |
Max serialized value size (0 = unlimited) |
WorkCount |
CPU*2 |
I/O threads per SocketManager |
ConnectRetry |
null |
Connection retry count |
CertificateValidation |
null |
TLS certificate validation callback |
CertificateSelection |
null |
TLS client certificate selection callback |
ConfigurationOptionsAsyncHandler |
null |
Async callback for custom ConfigurationOptions setup (e.g. Azure) |
Documentation
Full documentation is available in the doc/ folder:
Getting Started
Configuration
Serializers
Features
- Usage Guide — Add, Get, Replace, Bulk operations
- GeoSpatial Indexes
- VectorSet — AI/ML Similarity Search (Redis 8.0+)
- Redis Streams
- Pub/Sub Messaging
- HyperLogLog — Probabilistic cardinality estimation
- Distributed Lock — Redis-based locking with IAsyncDisposable
- Bitmap Operations — Bit-level analytics and feature flags
- Lua Scripting — Server-side script execution
- Hash Field Expiry (Redis 7.4+)
- Compression — GZip, Brotli, LZ4, Snappy, Zstandard
- Health Check
- IDistributedCache Adapter
Advanced
- Migration Guide: v12 → v13
- Migration Guide: v11 → v12
- Logging & Diagnostics
- Multiple Redis Servers — including Keyed DI Services
- Azure Managed Identity
- OpenTelemetry
- Redis Information Middleware
- NuGet Packages
Contributing
Thanks to all the people who already contributed!
<a href="https://github.com/imperugo/StackExchange.Redis.Extensions/graphs/contributors"> <img src="https://contributors-img.web.app/image?repo=imperugo/StackExchange.Redis.Extensions" /> </a>
Please read CONTRIBUTING.md before submitting a pull request. PRs target the master branch only.
License
StackExchange.Redis.Extensions is Copyright © Ugo Lattanzi and other contributors under the MIT license.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- StackExchange.Redis.Extensions.Core (>= 13.0.1)
-
net8.0
- StackExchange.Redis.Extensions.Core (>= 13.0.1)
-
net9.0
- StackExchange.Redis.Extensions.Core (>= 13.0.1)
NuGet packages (24)
Showing the top 5 NuGet packages that depend on StackExchange.Redis.Extensions.AspNetCore:
| Package | Downloads |
|---|---|
|
Xinghe.Utility
XH基础库(内部使用) |
|
|
WGM.Infrastructure.Core
Package Description |
|
|
ArkTribeManager.Core
Package Description |
|
|
Autransoft.Redis.InMemory
Package Description |
|
|
wfm-core
Package Description |
GitHub repositories (5)
Showing the top 5 popular GitHub repositories that depend on StackExchange.Redis.Extensions.AspNetCore:
| Repository | Stars |
|---|---|
|
Anapher/Strive
Open source video conferencing platform
|
|
|
Azure/sg-aks-workshop
Security + Governance Workshop
|
|
|
ncosentino/DevLeader
Projects referred to by my blog, Dev Leader
|
|
|
anton-martyniuk/Modern
.NET modern tools for fast and efficient development
|
|
|
239573049/EarthChat
|
| Version | Downloads | Last Updated |
|---|---|---|
| 13.0.1 | 99 | 8/3/2026 |
| 13.0.0 | 72 | 8/3/2026 |
| 12.6.0 | 10,951 | 7/9/2026 |
| 12.5.0 | 7,155 | 7/1/2026 |
| 12.2.0 | 68,784 | 5/22/2026 |
| 12.1.0 | 56,198 | 4/11/2026 |
| 12.0.0 | 4,284 | 4/11/2026 |
| 11.0.0 | 1,482,505 | 1/20/2025 |
| 10.2.0 | 2,337,779 | 1/23/2024 |
| 10.1.0 | 252,878 | 1/7/2024 |
| 10.0.2 | 87,951 | 12/25/2023 |
| 10.0.1 | 10,592 | 12/23/2023 |
| 10.0.0 | 21,721 | 12/23/2023 |
| 9.1.0 | 1,830,910 | 3/1/2023 |
| 9.0.0 | 42,436 | 2/20/2023 |
| 8.0.5 | 2,761,239 | 5/24/2022 |
| 8.0.4 | 695,162 | 2/14/2022 |
| 8.0.3 | 78,322 | 1/14/2022 |
| 8.0.2 | 62,325 | 1/8/2022 |
| 8.0.1 | 9,722 | 1/7/2022 |
v13.0.1:
- Performance: connection pool selection no longer allocates multiple ServerCounters snapshots per operation (LeastLoaded) and uses a true lock-free round-robin instead of a cryptographic RNG (RoundRobin)
- Performance: RedisDatabase instances are cached per database number instead of being allocated on every Db0..Db16 access
- Performance: HashGetAsync multi-key now issues a single HMGET instead of N parallel HGET round-trips
- Performance: UpdateExpiryAsync no longer issues a redundant EXISTS before EXPIRE (half the round-trips)
- Performance: AddAllAsync builds its payload without LINQ intermediate materializations
- Performance: eager materialization of previously lazy IEnumerable results (SetPopAsync, SortedSetRangeByScoreAsync, HashKeysAsync, SearchKeysAsync) — no more re-deserialization on repeated enumeration
- Performance: serializers (Newtonsoft, ServiceStack, MsgPack, Protobuf) no longer materialize intermediate UTF-16 strings or wrapping MemoryStreams
- Performance: compressors use pooled buffers (Snappier, Brotli, ZstdSharp) and reused Zstd contexts; Snappier.Decompress no longer duplicates the output array
- Performance: IDistributedCache adapter uses cached field arrays and batches HSET+EXPIRE in a single network write
- Fixed HashGetAllAsyncAtOneTimeAsync applying KeyPrefix to hash fields instead of the hash key, and removed a potential Lua injection through the hash key
- Fixed Utf8JsonSerializer.Deserialize throwing on null input instead of returning default
- Fixed ListGetFromRightAsync duplicated null check (dead code)
v13.0.0:
- Added HyperLogLog API: HyperLogLogAddAsync, HyperLogLogLengthAsync, HyperLogLogMergeAsync (#637)
- Added Distributed Lock API: LockTakeAsync, LockReleaseAsync, LockExtendAsync, LockQueryAsync, LockAcquireAsync with IAsyncDisposable auto-release (#638)
- Enriched Sorted Set API: SortedSetLengthAsync, SortedSetScoreAsync, SortedSetRankAsync, SortedSetIncrementAsync, SortedSetDecrementAsync, SortedSetPopAsync, SortedSetRandomMemberAsync, SortedSetCombineAndStoreAsync (#639)
- Enriched List API: ListAddToRightAsync, ListGetFromLeftAsync, ListLengthAsync, ListRangeAsync, ListGetByIndexAsync, ListSetByIndexAsync, ListTrimAsync, ListRemoveAsync, ListInsertBeforeAsync, ListInsertAfterAsync, ListMoveAsync (#640)
- Added Lua Scripting API: ScriptEvaluateAsync, ScriptEvaluateReadOnlyAsync with typed deserialization (#641)
- Added Bitmap API: StringSetBitAsync, StringGetBitAsync, StringBitCountAsync, StringBitOperationAsync, StringBitPositionAsync (#642)
- Added StackExchange.Redis 3.x compatibility via version range [2.12.14, 4.0.0) (#656)
- Added VectorSet tests now that Redis 8.0+ is available on Docker Hub (#648)
- Updated CI to use redis:8 Docker image for VectorSet test coverage
v12.6.0:
- Fixed Pub/Sub UnsubscribeAsync and UnsubscribeAllAsync not working with connection pooling — unsubscribe now iterates the pool to find the correct multiplexer (#657)
v12.5.0:
- Added IDistributedCache adapter with Hash-based storage compatible with Microsoft.Extensions.Caching.StackExchangeRedis (#632)
- Added ASP.NET Core Health Check for Redis connection pool monitoring (#633)
- Added StringIncrementAsync / StringDecrementAsync with long and double overloads (#634)
- Added SetCombineAsync / SetCombineAndStoreAsync for union, intersect, and difference operations (#635)
- Added Key Management: KeyRenameAsync, KeyTypeAsync, KeyDumpAsync, KeyRestoreAsync (#636)
- Added .NET 8+ Keyed DI Services for named Redis clients via [FromKeyedServices] (#654)
v12.2.0:
- Fixed SearchKeysAsync ignoring database number — SCAN now correctly targets the specified database instead of always scanning DB0 (#651)
- Fixed RemoveByTagAsync not deleting the tag Set key itself, causing a slow memory leak of empty tag Sets in Redis (#650)
- Upgraded Snappier to 1.3.x to fix high severity vulnerability (GHSA-pggp-6c3x-2xmx)
v12.1.0:
- Added VectorSet API for AI/ML similarity search (Redis 8.0+): VADD, VSIM, VREM, VCONTAINS, VCARD, VDIM, VGETATTR, VSETATTR, VINFO, VRANDMEMBER, VLINKS
- Added llms.txt for AI coding assistant documentation indexing
- Added Claude Code plugin with configure, scaffold, and diagnose skills
- Added complete API reference tables to all feature documentation
- Added SECURITY.md with GitHub Private Vulnerability Reporting
- Added CodeQL Advanced security analysis workflow
- Added CI workflow for automated testing on push/PR
v12.0.0:
- Added .NET 10 target framework
- Added GeoSpatial API (GEOADD, GEOSEARCH, GEODIST, GEOPOS, GEOHASH)
- Added Redis Streams API (XADD, XREAD, XREADGROUP, XACK, consumer groups)
- Added Hash field expiry (HEXPIRE, HSETEX, HPTTL, HPERSIST) for Redis 7.4+
- Added transparent compression support with pluggable ICompressor
- Added compression packages: LZ4, Snappy, Zstandard, GZip, Brotli
- Added Azure Managed Identity support via ConfigurationOptionsAsyncHandler
- Added CertificateSelection, ClientName, KeepAlive configuration properties
- Fixed SyncTimeout default from 1000ms to 5000ms
- Fixed Sentinel CommandMap blocking data commands (EVAL, GET, SET)
- Fixed AddAllAsync TTL race condition (now atomic per key)
- Fixed pool resilience: GetConnection skips disconnected connections
- Fixed PubSub handler silently swallowing exceptions
- Upgraded to StackExchange.Redis 2.12.14
- Upgraded to ConnectAsync (SE.Redis best practice)
- Replaced Moq with NSubstitute