From 36f915eb01f541af27df66573aae7fac0ef57834 Mon Sep 17 00:00:00 2001 From: "ugo.lattanzi" Date: Sat, 11 Apr 2026 14:50:46 +0200 Subject: [PATCH 1/6] Add llms.txt for AI indexing and Claude Code plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit llms.txt: - Follows llmstxt.org specification for AI assistant documentation - Links to all doc pages as raw GitHub URLs for direct AI consumption - Enables Context7 and other AI indexing services to find accurate, up-to-date API documentation Claude Code plugin (claude-plugin/): - plugin.json with metadata for marketplace publishing - skills/configure — generate RedisConfiguration and DI setup from natural language (serializer/compressor selection guide included) - skills/scaffold — production-ready code patterns: cache-aside, consumer groups, geo search, VectorSet RAG, hash field TTL, Pub/Sub - skills/diagnose — troubleshooting tree for timeouts, connection failures, serialization errors, pool exhaustion, Pub/Sub issues README.md: - Added "AI-Ready" callout with links to llms.txt and plugin Closes #629 Closes #630 --- README.md | 2 + claude-plugin/plugin.json | 12 ++ claude-plugin/skills/configure/SKILL.md | 115 ++++++++++++++++ claude-plugin/skills/diagnose/SKILL.md | 106 +++++++++++++++ claude-plugin/skills/scaffold/SKILL.md | 171 ++++++++++++++++++++++++ llms.txt | 42 ++++++ 6 files changed, 448 insertions(+) create mode 100644 claude-plugin/plugin.json create mode 100644 claude-plugin/skills/configure/SKILL.md create mode 100644 claude-plugin/skills/diagnose/SKILL.md create mode 100644 claude-plugin/skills/scaffold/SKILL.md create mode 100644 llms.txt diff --git a/README.md b/README.md index 780f3c6..c3b3730 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ ![Tests](https://img.shields.io/badge/tests-970%2B%20passing-brightgreen) ![.NET](https://img.shields.io/badge/.NET-8%20%7C%209%20%7C%2010-blue) +> **AI-Ready:** This library provides an [`llms.txt`](llms.txt) file for AI coding assistants and a [Claude Code plugin](claude-plugin/) for configuration, scaffolding, and troubleshooting. + ## Features - Store and retrieve complex .NET objects with automatic serialization diff --git a/claude-plugin/plugin.json b/claude-plugin/plugin.json new file mode 100644 index 0000000..c328dc7 --- /dev/null +++ b/claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "stackexchange-redis-extensions", + "version": "1.0.0", + "description": "Configure, scaffold, and troubleshoot StackExchange.Redis.Extensions — the high-level .NET Redis client with serialization, pooling, Streams, Geo, VectorSet, compression, and more.", + "author": "imperugo", + "homepage": "https://github.com/imperugo/StackExchange.Redis.Extensions", + "skills": [ + "skills/configure", + "skills/scaffold", + "skills/diagnose" + ] +} diff --git a/claude-plugin/skills/configure/SKILL.md b/claude-plugin/skills/configure/SKILL.md new file mode 100644 index 0000000..126d1a6 --- /dev/null +++ b/claude-plugin/skills/configure/SKILL.md @@ -0,0 +1,115 @@ +--- +name: redis-configure +description: Generate StackExchange.Redis.Extensions configuration and DI registration from natural language requirements +--- + +# Redis Configure + +Generate complete `RedisConfiguration` and ASP.NET Core DI setup for StackExchange.Redis.Extensions based on the user's requirements. + +## When to use + +When the user asks to: +- Set up Redis in their .NET project +- Configure connection pooling, Sentinel, TLS, Azure Managed Identity +- Choose a serializer or compressor +- Set up multiple Redis instances + +## How to respond + +1. Ask clarifying questions if needed (single vs. multi-instance, cloud vs. local, auth method) +2. Generate the complete configuration + +## Configuration Reference + +### NuGet packages required +- `StackExchange.Redis.Extensions.Core` — always required +- `StackExchange.Redis.Extensions.AspNetCore` — for DI registration +- One serializer: `System.Text.Json` (recommended), `Newtonsoft`, `MemoryPack`, `MsgPack`, `Protobuf` +- Optional compressor: `Compression.LZ4` (fastest), `Compression.ZstdSharp` (best ratio), `Compression.GZip` (no deps) + +### appsettings.json structure +```json +{ + "Redis": { + "Password": "", + "AllowAdmin": true, + "Ssl": false, + "ConnectTimeout": 5000, + "SyncTimeout": 5000, + "Database": 0, + "Hosts": [{ "Host": "localhost", "Port": 6379 }], + "PoolSize": 5, + "IsDefault": true + } +} +``` + +### Program.cs registration +```csharp +var redisConfig = builder.Configuration.GetSection("Redis").Get(); +builder.Services.AddStackExchangeRedisExtensions(redisConfig); +// Optional compression: +builder.Services.AddRedisCompression(); +``` + +### Azure Managed Identity +```csharp +redisConfig.ConfigurationOptionsAsyncHandler = async opts => +{ + await opts.ConfigureForAzureWithTokenCredentialAsync(new DefaultAzureCredential()); + return opts; +}; +``` + +### Sentinel configuration +```csharp +var config = new RedisConfiguration +{ + ServiceName = "mymaster", + Hosts = new[] { + new RedisHost { Host = "sentinel1", Port = 26379 }, + new RedisHost { Host = "sentinel2", Port = 26379 }, + }, + IsDefault = true, +}; +``` + +### Multiple instances +```csharp +var configs = new[] +{ + new RedisConfiguration { Name = "Cache", IsDefault = true, /* ... */ }, + new RedisConfiguration { Name = "Session", /* ... */ }, +}; +builder.Services.AddStackExchangeRedisExtensions(configs); + +// Resolve: inject IRedisClientFactory, call GetRedisClient("Session") +``` + +### Key properties +| Property | Default | Description | +|----------|---------|-------------| +| PoolSize | 5 | Connection pool size | +| ConnectionSelectionStrategy | LeastLoaded | LeastLoaded or RoundRobin | +| SyncTimeout | 5000 | Sync timeout (ms) | +| ConnectTimeout | 5000 | Connect timeout (ms) | +| KeyPrefix | "" | Prefix for all keys and channels | +| KeepAlive | -1 | Heartbeat interval (seconds) | +| ClientName | null | Connection client name | +| MaxValueLength | 0 | Max serialized value size (0 = unlimited) | + +### Serializer selection guide +| Scenario | Recommended | +|----------|-------------| +| General purpose | System.Text.Json | +| Legacy JSON.NET compatibility | Newtonsoft | +| Maximum performance (binary) | MemoryPack (net7.0+) | +| Cross-language compatibility | Protobuf or MsgPack | + +### Compressor selection guide +| Scenario | Recommended | +|----------|-------------| +| Lowest latency (caching) | LZ4 | +| Best compression ratio | ZstdSharp or Brotli | +| No external dependencies | GZip | diff --git a/claude-plugin/skills/diagnose/SKILL.md b/claude-plugin/skills/diagnose/SKILL.md new file mode 100644 index 0000000..e5d6ae5 --- /dev/null +++ b/claude-plugin/skills/diagnose/SKILL.md @@ -0,0 +1,106 @@ +--- +name: redis-diagnose +description: Troubleshoot common StackExchange.Redis.Extensions issues — timeouts, connection failures, serialization problems, pool exhaustion +--- + +# Redis Diagnose + +Diagnose and fix common issues with StackExchange.Redis.Extensions. + +## When to use + +When the user reports: +- Timeout exceptions +- Connection failures +- Serialization errors +- Pool exhaustion +- Pub/Sub messages not being received +- Performance issues + +## Diagnostic Tree + +### RedisTimeoutException +**Symptoms:** `StackExchange.Redis.RedisTimeoutException` + +**Check in order:** +1. **SyncTimeout too low** — default is 5000ms, increase for slow networks + ```csharp + config.SyncTimeout = 10000; // 10 seconds + ``` +2. **Pool size too small** — default 5, increase for high-throughput + ```csharp + config.PoolSize = 10; + ``` +3. **Connection strategy** — switch to LeastLoaded if using RoundRobin + ```csharp + config.ConnectionSelectionStrategy = ConnectionSelectionStrategy.LeastLoaded; + ``` +4. **Large values** — enable compression to reduce payload size + ```csharp + services.AddRedisCompression(); + ``` +5. **ThreadPool starvation** — check with `ThreadPool.GetAvailableThreads()`, increase min threads + +### RedisConnectionException +**Symptoms:** `SocketClosed`, `ConnectionFailed` + +**Check:** +1. **Redis server reachable?** — `redis-cli -h -p ping` +2. **Firewall/NSG rules** — port 6379 (or 6380 for TLS) open? +3. **TLS misconfiguration** — if using Ssl=true, check certificate callbacks +4. **Sentinel misconfiguration** — ServiceName must match Redis master name exactly +5. **Azure Cache for Redis** — ensure Managed Identity is configured if not using password + +### Serialization Errors +**Symptoms:** `JsonException`, `InvalidOperationException`, corrupt data + +**Check:** +1. **Type mismatch** — GetAsync must use same T as AddAsync +2. **String values are JSON-encoded** — "hello" is stored as "\"hello\"", this is by design +3. **Compression migration** — enabling compression makes old (uncompressed) data unreadable + - Error: `InvalidOperationException: Failed to decompress data from Redis` + - Fix: flush the database or read old data without compression first +4. **Value type quirk** — `GetAsync()` returns 0 (not null) for missing keys. Use `GetAsync()` instead. + +### Pub/Sub Not Receiving Messages +**Check:** +1. **KeyPrefix** — channels are automatically prefixed. Don't add prefix manually. +2. **Serializer mismatch** — publisher and subscriber must use the same serializer +3. **Handler exceptions** — check logs for EventId 4001 errors. Handlers that throw don't crash but the message is lost. +4. **Different connection pools** — ensure pub and sub use the same IRedisDatabase instance + +### Pool Exhaustion / All Connections Down +**Symptoms:** All operations fail, logs show EventId 1003 + +**Check:** +1. **Pool health** — `redis.ConnectionPoolManager.GetConnectionInformation()` +2. **Redis server overloaded** — check `INFO clients` on Redis +3. **Network partition** — the pool skips disconnected connections automatically and logs warnings +4. **Dispose pattern** — ensure IRedisConnectionPoolManager is not disposed prematurely + +### Performance Issues +**Check:** +1. **Enable logging** — set log level to Debug to see connection selection + ```json + { "Logging": { "LogLevel": { "StackExchange.Redis.Extensions.Core": "Debug" } } } + ``` +2. **Check outstanding commands** — pool info shows outstanding count per connection +3. **Use compression** for large objects — LZ4 adds ~1ms latency but reduces network 5-10x +4. **Use AddAllAsync** for bulk writes instead of loop of AddAsync +5. **Use GetAllAsync** with HashSet for bulk reads + +## Logging Reference + +| EventId | Level | Meaning | +|---------|-------|---------| +| 1001 | Info | Pool initialized successfully | +| 1003 | Warning | All connections disconnected — degraded mode | +| 1006 | Error | Pool initialization failed | +| 2001 | Error | Connection failed | +| 2002 | Warning | Connection restored | +| 4001 | Error | Pub/Sub handler threw exception | + +Enable with: +```json +{ "Logging": { "LogLevel": { "StackExchange.Redis.Extensions.Core": "Information" } } } +``` diff --git a/claude-plugin/skills/scaffold/SKILL.md b/claude-plugin/skills/scaffold/SKILL.md new file mode 100644 index 0000000..bc359e8 --- /dev/null +++ b/claude-plugin/skills/scaffold/SKILL.md @@ -0,0 +1,171 @@ +--- +name: redis-scaffold +description: Generate code patterns for StackExchange.Redis.Extensions — Streams, Geo, VectorSet, Hash, Pub/Sub, Sets, Compression +--- + +# Redis Scaffold + +Generate production-ready code patterns using StackExchange.Redis.Extensions. + +## When to use + +When the user asks to implement: +- Redis Streams consumer group workflow +- GeoSpatial search (nearby locations) +- VectorSet similarity search (RAG, recommendations) +- Hash operations with per-field TTL +- Pub/Sub messaging +- Caching patterns (cache-aside, write-through) +- Bulk operations + +## Patterns + +### Cache-Aside Pattern +```csharp +public class ProductService(IRedisDatabase redis, IProductRepository repo) +{ + public async Task GetProductAsync(int id) + { + var key = $"product:{id}"; + var cached = await redis.GetAsync(key); + + if (cached is not null) + return cached; + + var product = await repo.GetByIdAsync(id); + + if (product is not null) + await redis.AddAsync(key, product, TimeSpan.FromMinutes(30)); + + return product; + } +} +``` + +### Consumer Group (Streams) +```csharp +public class OrderProcessor(IRedisDatabase redis) +{ + public async Task ProcessAsync(CancellationToken ct) + { + await redis.StreamCreateConsumerGroupAsync("orders", "processors", "0-0", createStream: true); + + while (!ct.IsCancellationRequested) + { + var entries = await redis.StreamReadGroupAsync("orders", "processors", "worker-1", ">", count: 10); + + foreach (var entry in entries) + { + try + { + // Process message + await redis.StreamAcknowledgeAsync("orders", "processors", entry.Id!); + } + catch + { + // Message stays in PEL for retry + } + } + + if (entries.Length == 0) + await Task.Delay(1000, ct); + } + } +} +``` + +### GeoSpatial Search +```csharp +public class StoreLocator(IRedisDatabase redis) +{ + public async Task FindNearbyAsync(double lat, double lon, double radiusKm) + { + return await redis.GeoSearchAsync("stores", lon, lat, + new GeoSearchCircle(radiusKm, GeoUnit.Kilometers), + count: 20, order: Order.Ascending); + } + + public async Task AddStoreAsync(string id, double lat, double lon) + { + await redis.GeoAddAsync("stores", lon, lat, id); + } +} +``` + +### VectorSet Similarity Search (RAG) +```csharp +public class DocumentSearch(IRedisDatabase redis) +{ + public async Task IndexAsync(string docId, float[] embedding, string title) + { + await redis.VectorSetAddAsync("docs", + VectorSetAddRequest.Member(docId, embedding, + attributes: $"""{{ "title": "{title}" }}""")); + } + + public async Task> SearchAsync(float[] queryVector, int topK = 5) + { + using var results = await redis.VectorSetSimilaritySearchAsync("docs", + VectorSetSimilaritySearchRequest.ByVector(queryVector) with { Count = topK }); + + var items = new List<(string, double)>(); + if (results is not null) + foreach (var r in results.Span) + items.Add((r.Member!, r.Score)); + + return items; + } +} +``` + +### Hash with Per-Field TTL +```csharp +public class SessionStore(IRedisDatabase redis) +{ + public async Task SetSessionDataAsync(string userId, string token, object profile) + { + var hashKey = $"user:{userId}"; + + // Permanent profile data + await redis.HashSetAsync(hashKey, "profile", profile); + + // Session token expires in 30 minutes + await redis.HashSetWithExpiryAsync(hashKey, "token", token, TimeSpan.FromMinutes(30)); + } +} +``` + +### Pub/Sub with Typed Messages +```csharp +public class EventBus(IRedisDatabase redis) +{ + public async Task PublishAsync(string channel, T message) + { + await redis.PublishAsync(new RedisChannel(channel, RedisChannel.PatternMode.Literal), message); + } + + public async Task SubscribeAsync(string channel, Func handler) + { + await redis.SubscribeAsync(new RedisChannel(channel, RedisChannel.PatternMode.Literal), handler); + } +} +``` + +### Bulk Operations with Expiry +```csharp +public async Task CacheBulkAsync(IRedisDatabase redis, Dictionary products) +{ + var items = products.Select(p => Tuple.Create($"product:{p.Key}", p.Value)).ToArray(); + await redis.AddAllAsync(items, TimeSpan.FromHours(1)); +} +``` + +## Important Notes + +- All values go through ISerializer — strings are JSON-encoded ("hello" → "\"hello\"") +- For raw Redis operations, use `redis.Database` directly +- KeyPrefix applies to both keys AND Pub/Sub channels +- VectorSet requires Redis 8.0+ +- Hash field expiry requires Redis 7.4+ +- Compression wraps ISerializer transparently — all operations benefit automatically +- Lease return types (VectorSet search) must be disposed after use diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..a96aad8 --- /dev/null +++ b/llms.txt @@ -0,0 +1,42 @@ +# StackExchange.Redis.Extensions + +> A .NET library that extends StackExchange.Redis with object serialization, connection pooling, and higher-level APIs for Redis operations including Pub/Sub, Streams, GeoSpatial, VectorSet, Hash field expiry, and transparent compression. + +StackExchange.Redis.Extensions wraps the base StackExchange.Redis client library to make it easier to work with Redis in .NET applications. It supports .NET Standard 2.1, .NET 8, .NET 9, and .NET 10. All values are automatically serialized/deserialized through a pluggable ISerializer interface. Connection pooling with LeastLoaded and RoundRobin strategies is built-in. + +## Core Documentation + +- [README — Quick Start, Architecture, Configuration](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/README.md): Full getting started guide with DI setup, usage examples, and configuration reference table +- [Setup & Installation](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/setup-1.md): Package installation and basic setup +- [Dependency Injection](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/dependency-injection.md): ASP.NET Core DI registration + +## Configuration + +- [JSON Configuration](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/configuration/json-configuration.md): appsettings.json configuration +- [C# Configuration](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/configuration/c-configuration.md): Programmatic RedisConfiguration setup +- [Connection Pool](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/configuration/connection-pool.md): Pool size, strategies, health monitoring + +## Features + +- [Usage Guide](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/usage/add-and-retrieve-complex-object-to-redis.md): Add, retrieve, and remove objects +- [Work with Multiple Items](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/usage/work-with-multiple-items.md): Bulk operations with AddAllAsync +- [GeoSpatial Indexes](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/geospatial.md): GEOADD, GEOSEARCH, GEODIST, GEOPOS +- [VectorSet — AI/ML Similarity Search](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/vectorset.md): VADD, VSIM for RAG, recommendations, semantic search (Redis 8.0+) +- [Redis Streams](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/streams.md): XADD, XREAD, consumer groups, XACK +- [Pub/Sub Messaging](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/pubsub.md): Typed publish/subscribe with error handling +- [Hash Field Expiry](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/hash-field-expiry.md): Per-field TTL with HSETEX, HEXPIRE (Redis 7.4+) +- [Compression](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/compressors.md): LZ4, Snappy, Zstandard, GZip, Brotli + +## Serializers + +- [Serializers Overview](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/serializers/README.md): ISerializer interface and available implementations +- [System.Text.Json](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/serializers/system.text.json.md): Recommended serializer +- [Custom Serializer](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/usage/custom-serializer.md): Implement your own ISerializer + +## Advanced + +- [Azure Managed Identity](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/azure-managed-identity.md): ConfigurationOptionsAsyncHandler for Azure Cache +- [Multiple Redis Servers](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/multipleServers.md): Named Redis instances with IRedisClientFactory +- [Logging & Diagnostics](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/logging.md): Source-generated logging, event IDs, filtering +- [OpenTelemetry](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/openTelemetry.md): Distributed tracing integration +- [ASP.NET Core Middleware](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/asp.net-core/README.md): Integration and middleware setup From 7aee47836fe4b11d21cb1c68c41d36bf3f53616f Mon Sep 17 00:00:00 2001 From: "ugo.lattanzi" Date: Sat, 11 Apr 2026 14:51:56 +0200 Subject: [PATCH 2/6] Add VectorSet documentation link to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c3b3730..4abf81b 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,7 @@ Full documentation is available in the [doc/](doc/) folder: **Features** - [Usage Guide](doc/usage/README.md) — Add, Get, Replace, Bulk operations - [GeoSpatial Indexes](doc/geospatial.md) +- [VectorSet — AI/ML Similarity Search](doc/vectorset.md) (Redis 8.0+) - [Redis Streams](doc/streams.md) - [Pub/Sub Messaging](doc/pubsub.md) - [Hash Field Expiry](doc/hash-field-expiry.md) (Redis 7.4+) From 1de77901e0a8311037579ddc5abece6a92e5efba Mon Sep 17 00:00:00 2001 From: "ugo.lattanzi" Date: Sat, 11 Apr 2026 14:52:47 +0200 Subject: [PATCH 3/6] Add Claude Code plugin install instructions to README --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4abf81b..c2fa1f2 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,12 @@ ![Tests](https://img.shields.io/badge/tests-970%2B%20passing-brightgreen) ![.NET](https://img.shields.io/badge/.NET-8%20%7C%209%20%7C%2010-blue) -> **AI-Ready:** This library provides an [`llms.txt`](llms.txt) file for AI coding assistants and a [Claude Code plugin](claude-plugin/) for configuration, scaffolding, and troubleshooting. +> **AI-Ready:** This library provides an [`llms.txt`](llms.txt) file for AI coding assistants and a Claude Code plugin for configuration, scaffolding, and troubleshooting. +> +> ```bash +> claude plugin add imperugo/StackExchange.Redis.Extensions +> ``` +> Then use `/redis-configure`, `/redis-scaffold`, or `/redis-diagnose` in Claude Code. ## Features From 4ae1715abea596bb26c38e527a40d9c0127262ae Mon Sep 17 00:00:00 2001 From: "ugo.lattanzi" Date: Sat, 11 Apr 2026 15:07:56 +0200 Subject: [PATCH 4/6] Fix review findings: ConnectionPoolManager path, package names, code examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix diagnose skill: ConnectionPoolManager is on IRedisClient, not IRedisDatabase - Fix llms.txt: Snappy → Snappier (matches actual package name) - Fix configure skill: add missing packages (ServiceStack, Utf8Json, Snappier, Brotli) - Fix configure skill: MemoryPack net7.0+ → net8.0+ (matches actual TFMs) - Fix scaffold skill: add comment explaining ">" in StreamReadGroupAsync - Fix scaffold skill: entry.Id! → entry.Id.ToString() (RedisValue is a struct) - Fix diagnose skill: expand GetAsync value type explanation - Fix diagnose skill: clarify GetAllAsync requires HashSet --- claude-plugin/skills/configure/SKILL.md | 6 +++--- claude-plugin/skills/diagnose/SKILL.md | 6 +++--- claude-plugin/skills/scaffold/SKILL.md | 4 ++-- llms.txt | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/claude-plugin/skills/configure/SKILL.md b/claude-plugin/skills/configure/SKILL.md index 126d1a6..71e0875 100644 --- a/claude-plugin/skills/configure/SKILL.md +++ b/claude-plugin/skills/configure/SKILL.md @@ -25,8 +25,8 @@ When the user asks to: ### NuGet packages required - `StackExchange.Redis.Extensions.Core` — always required - `StackExchange.Redis.Extensions.AspNetCore` — for DI registration -- One serializer: `System.Text.Json` (recommended), `Newtonsoft`, `MemoryPack`, `MsgPack`, `Protobuf` -- Optional compressor: `Compression.LZ4` (fastest), `Compression.ZstdSharp` (best ratio), `Compression.GZip` (no deps) +- One serializer: `System.Text.Json` (recommended), `Newtonsoft`, `MemoryPack`, `MsgPack`, `Protobuf`, `ServiceStack`, `Utf8Json` +- Optional compressor: `Compression.LZ4` (fastest), `Compression.Snappier`, `Compression.ZstdSharp` (best ratio), `Compression.GZip` (no deps), `Compression.Brotli` (best ratio for text) ### appsettings.json structure ```json @@ -104,7 +104,7 @@ builder.Services.AddStackExchangeRedisExtensions(confi |----------|-------------| | General purpose | System.Text.Json | | Legacy JSON.NET compatibility | Newtonsoft | -| Maximum performance (binary) | MemoryPack (net7.0+) | +| Maximum performance (binary) | MemoryPack (net8.0+) | | Cross-language compatibility | Protobuf or MsgPack | ### Compressor selection guide diff --git a/claude-plugin/skills/diagnose/SKILL.md b/claude-plugin/skills/diagnose/SKILL.md index e5d6ae5..e12b0b9 100644 --- a/claude-plugin/skills/diagnose/SKILL.md +++ b/claude-plugin/skills/diagnose/SKILL.md @@ -60,7 +60,7 @@ When the user reports: 3. **Compression migration** — enabling compression makes old (uncompressed) data unreadable - Error: `InvalidOperationException: Failed to decompress data from Redis` - Fix: flush the database or read old data without compression first -4. **Value type quirk** — `GetAsync()` returns 0 (not null) for missing keys. Use `GetAsync()` instead. +4. **Value type quirk** — `GetAsync()` returns 0 (not null) for missing keys because `default(int)` is `0`. Use `GetAsync()` to distinguish missing keys from actual zero values. ### Pub/Sub Not Receiving Messages **Check:** @@ -73,7 +73,7 @@ When the user reports: **Symptoms:** All operations fail, logs show EventId 1003 **Check:** -1. **Pool health** — `redis.ConnectionPoolManager.GetConnectionInformation()` +1. **Pool health** — inject `IRedisClient` and call `client.ConnectionPoolManager.GetConnectionInformation()` 2. **Redis server overloaded** — check `INFO clients` on Redis 3. **Network partition** — the pool skips disconnected connections automatically and logs warnings 4. **Dispose pattern** — ensure IRedisConnectionPoolManager is not disposed prematurely @@ -87,7 +87,7 @@ When the user reports: 2. **Check outstanding commands** — pool info shows outstanding count per connection 3. **Use compression** for large objects — LZ4 adds ~1ms latency but reduces network 5-10x 4. **Use AddAllAsync** for bulk writes instead of loop of AddAsync -5. **Use GetAllAsync** with HashSet for bulk reads +5. **Use GetAllAsync** for bulk reads (note: requires `HashSet` for keys, not arrays) ## Logging Reference diff --git a/claude-plugin/skills/scaffold/SKILL.md b/claude-plugin/skills/scaffold/SKILL.md index bc359e8..ffb2bc0 100644 --- a/claude-plugin/skills/scaffold/SKILL.md +++ b/claude-plugin/skills/scaffold/SKILL.md @@ -52,14 +52,14 @@ public class OrderProcessor(IRedisDatabase redis) while (!ct.IsCancellationRequested) { - var entries = await redis.StreamReadGroupAsync("orders", "processors", "worker-1", ">", count: 10); + var entries = await redis.StreamReadGroupAsync("orders", "processors", "worker-1", ">", count: 10); // ">" = read only new messages foreach (var entry in entries) { try { // Process message - await redis.StreamAcknowledgeAsync("orders", "processors", entry.Id!); + await redis.StreamAcknowledgeAsync("orders", "processors", entry.Id.ToString()); } catch { diff --git a/llms.txt b/llms.txt index a96aad8..f9a641c 100644 --- a/llms.txt +++ b/llms.txt @@ -25,7 +25,7 @@ StackExchange.Redis.Extensions wraps the base StackExchange.Redis client library - [Redis Streams](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/streams.md): XADD, XREAD, consumer groups, XACK - [Pub/Sub Messaging](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/pubsub.md): Typed publish/subscribe with error handling - [Hash Field Expiry](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/hash-field-expiry.md): Per-field TTL with HSETEX, HEXPIRE (Redis 7.4+) -- [Compression](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/compressors.md): LZ4, Snappy, Zstandard, GZip, Brotli +- [Compression](https://raw.githubusercontent.com/imperugo/StackExchange.Redis.Extensions/master/doc/compressors.md): LZ4, Snappier, Zstandard, GZip, Brotli ## Serializers From 4734135ea487a3dfd99e7c76a50ac2ebb466e760 Mon Sep 17 00:00:00 2001 From: "ugo.lattanzi" Date: Sat, 11 Apr 2026 15:12:43 +0200 Subject: [PATCH 5/6] Enrich documentation with complete API reference tables Each feature doc page now includes a full API Reference table with method signatures, Redis commands, and return types. This ensures AI coding assistants (via llms.txt/Context7) have accurate method signatures, not just tutorial examples. - doc/geospatial.md: 16-method API reference table - doc/streams.md: 16-method API reference table - doc/vectorset.md: 14-method API reference table with Lease notes - doc/compressors.md: full NuGet package names, class names, install commands for all 5 compressor packages --- doc/compressors.md | 25 ++++++++++++++++++------- doc/geospatial.md | 20 ++++++++++++++++++++ doc/streams.md | 21 +++++++++++++++++++++ doc/vectorset.md | 21 +++++++++++++++++++++ 4 files changed, 80 insertions(+), 7 deletions(-) diff --git a/doc/compressors.md b/doc/compressors.md index 8fd71b4..8932937 100644 --- a/doc/compressors.md +++ b/doc/compressors.md @@ -16,13 +16,24 @@ graph LR ## Available Compressors -| Package | Algorithm | Speed | Ratio | Dependencies | -|---------|----------|-------|-------|-------------| -| `Compression.LZ4` | LZ4 | Fastest | Lower | K4os.Compression.LZ4 | -| `Compression.Snappier` | Snappy | Very Fast | Lower | Snappier | -| `Compression.ZstdSharp` | Zstandard | Fast | Good | ZstdSharp.Port | -| `Compression.GZip` | GZip | Moderate | Good | None (BCL) | -| `Compression.Brotli` | Brotli | Slower | Best | None (BCL) | +| NuGet Package | Class | Algorithm | Speed | Ratio | +|---------------|-------|----------|-------|-------| +| `StackExchange.Redis.Extensions.Compression.LZ4` | `LZ4Compressor` | LZ4 | Fastest | Lower | +| `StackExchange.Redis.Extensions.Compression.Snappier` | `SnappierCompressor` | Snappy | Very Fast | Lower | +| `StackExchange.Redis.Extensions.Compression.ZstdSharp` | `ZstdSharpCompressor` | Zstandard | Fast | Good | +| `StackExchange.Redis.Extensions.Compression.GZip` | `GZipCompressor` | GZip | Moderate | Good | +| `StackExchange.Redis.Extensions.Compression.Brotli` | `BrotliCompressor` | Brotli | Slower | Best | + +### Install + +```bash +# Pick one: +dotnet add package StackExchange.Redis.Extensions.Compression.LZ4 +dotnet add package StackExchange.Redis.Extensions.Compression.Snappier +dotnet add package StackExchange.Redis.Extensions.Compression.ZstdSharp +dotnet add package StackExchange.Redis.Extensions.Compression.GZip +dotnet add package StackExchange.Redis.Extensions.Compression.Brotli +``` ### Recommendations diff --git a/doc/geospatial.md b/doc/geospatial.md index ce3dab7..2046c0f 100644 --- a/doc/geospatial.md +++ b/doc/geospatial.md @@ -93,3 +93,23 @@ graph LR ``` > **Note:** Geo members are string identifiers (e.g., store names, IDs). Coordinates are stored internally by Redis as sorted set scores. If you need to associate complex objects with a location, store them under a key derived from the member name. + +## API Reference + +| Method | Redis Command | Parameters | Returns | +|--------|--------------|------------|---------| +| `GeoAddAsync(key, longitude, latitude, member)` | GEOADD | `string key, double lon, double lat, string member` | `Task` | +| `GeoAddAsync(key, value)` | GEOADD | `string key, GeoEntry value` | `Task` | +| `GeoAddAsync(key, values)` | GEOADD | `string key, GeoEntry[] values` | `Task` | +| `GeoRemoveAsync(key, member)` | ZREM | `string key, string member` | `Task` | +| `GeoDistanceAsync(key, member1, member2, unit)` | GEODIST | `string key, string m1, string m2, GeoUnit unit` | `Task` | +| `GeoHashAsync(key, member)` | GEOHASH | `string key, string member` | `Task` | +| `GeoHashAsync(key, members)` | GEOHASH | `string key, string[] members` | `Task` | +| `GeoPositionAsync(key, member)` | GEOPOS | `string key, string member` | `Task` | +| `GeoPositionAsync(key, members)` | GEOPOS | `string key, string[] members` | `Task` | +| `GeoRadiusAsync(key, member, radius, ...)` | GEORADIUS | by member | `Task` | +| `GeoRadiusAsync(key, lon, lat, radius, ...)` | GEORADIUS | by coordinates | `Task` | +| `GeoSearchAsync(key, member, shape, ...)` | GEOSEARCH | by member + shape | `Task` | +| `GeoSearchAsync(key, lon, lat, shape, ...)` | GEOSEARCH | by coordinates + shape | `Task` | +| `GeoSearchAndStoreAsync(src, dst, member, shape, ...)` | GEOSEARCHSTORE | by member | `Task` | +| `GeoSearchAndStoreAsync(src, dst, lon, lat, shape, ...)` | GEOSEARCHSTORE | by coordinates | `Task` | diff --git a/doc/streams.md b/doc/streams.md index 8fce81a..0e79bf2 100644 --- a/doc/streams.md +++ b/doc/streams.md @@ -157,3 +157,24 @@ foreach (var entry in entries) ``` > **Note:** For advanced operations (XCLAIM, XAUTOCLAIM, XINFO), use `redis.Database` to access the underlying StackExchange.Redis `IDatabase` directly. + +## API Reference + +| Method | Redis Command | Returns | +|--------|--------------|---------| +| `StreamAddAsync(key, fieldName, value, ...)` | XADD | `Task` (message ID) | +| `StreamAddAsync(key, entries, ...)` | XADD | `Task` (message ID) | +| `StreamLengthAsync(key)` | XLEN | `Task` | +| `StreamTrimAsync(key, maxLength, ...)` | XTRIM | `Task` (entries removed) | +| `StreamDeleteAsync(key, messageIds)` | XDEL | `Task` (entries deleted) | +| `StreamRangeAsync(key, minId, maxId, count, order)` | XRANGE/XREVRANGE | `Task` | +| `StreamReadAsync(key, position, count)` | XREAD | `Task` | +| `StreamCreateConsumerGroupAsync(key, groupName, position, createStream)` | XGROUP CREATE | `Task` | +| `StreamConsumerGroupSetPositionAsync(key, groupName, position)` | XGROUP SETID | `Task` | +| `StreamDeleteConsumerGroupAsync(key, groupName)` | XGROUP DESTROY | `Task` | +| `StreamDeleteConsumerAsync(key, groupName, consumerName)` | XGROUP DELCONSUMER | `Task` | +| `StreamReadGroupAsync(key, groupName, consumerName, position, count, noAck)` | XREADGROUP | `Task` | +| `StreamAcknowledgeAsync(key, groupName, messageId)` | XACK | `Task` | +| `StreamAcknowledgeAsync(key, groupName, messageIds)` | XACK | `Task` | +| `StreamPendingAsync(key, groupName)` | XPENDING | `Task` | +| `StreamPendingMessagesAsync(key, groupName, count, consumerName, ...)` | XPENDING | `Task` | diff --git a/doc/vectorset.md b/doc/vectorset.md index 9f8129a..47a4bbb 100644 --- a/doc/vectorset.md +++ b/doc/vectorset.md @@ -134,6 +134,27 @@ using var results = await redis.VectorSetSimilaritySearchAsync("clothing", VectorSetSimilaritySearchRequest.ByVector(searchEmb) with { Count = 20 }); ``` +## API Reference + +| Method | Redis Command | Returns | +|--------|--------------|---------| +| `VectorSetAddAsync(key, request)` | VADD | `Task` | +| `VectorSetSimilaritySearchAsync(key, query)` | VSIM | `Task?>` (dispose!) | +| `VectorSetRemoveAsync(key, member)` | VREM | `Task` | +| `VectorSetContainsAsync(key, member)` | VCONTAINS | `Task` | +| `VectorSetLengthAsync(key)` | VCARD | `Task` | +| `VectorSetDimensionAsync(key)` | VDIM | `Task` | +| `VectorSetGetAttributesJsonAsync(key, member)` | VGETATTR | `Task` | +| `VectorSetSetAttributesJsonAsync(key, member, json)` | VSETATTR | `Task` | +| `VectorSetInfoAsync(key)` | VINFO | `Task` | +| `VectorSetRandomMemberAsync(key)` | VRANDMEMBER | `Task` | +| `VectorSetRandomMembersAsync(key, count)` | VRANDMEMBER | `Task` | +| `VectorSetGetApproximateVectorAsync(key, member)` | VGETAPPROX | `Task?>` (dispose!) | +| `VectorSetGetLinksAsync(key, member)` | VLINKS | `Task?>` (dispose!) | +| `VectorSetGetLinksWithScoresAsync(key, member)` | VLINKS WITHSCORES | `Task?>` (dispose!) | + +**SE.Redis types used:** `VectorSetAddRequest.Member(member, vector)`, `VectorSetSimilaritySearchRequest.ByVector(vector)`, `Lease` (IDisposable — always use `using`). + ## Performance Notes - VectorSet uses HNSW (Hierarchical Navigable Small World) algorithm internally From 77c21313d317cbe009036e4868ae4f5814557b87 Mon Sep 17 00:00:00 2001 From: "ugo.lattanzi" Date: Sat, 11 Apr 2026 15:17:19 +0200 Subject: [PATCH 6/6] =?UTF-8?q?Bump=20version=20to=2012.1.0,=20add=20migra?= =?UTF-8?q?tion=20guide=20v11=E2=86=92v12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - VersionPrefix 12.0.0 → 12.1.0 - PackageReleaseNotes updated with v12.1.0 changes (VectorSet, llms.txt, Claude plugin, CodeQL, SECURITY.md, CI workflow) - doc/migration-v11-to-v12.md: comprehensive migration guide covering SyncTimeout change, Sentinel fix, SE.Redis upgrade, all new features, dependency changes, FAQ - Linked migration guide from README and doc/README.md --- Directory.Build.props | 11 ++- README.md | 1 + doc/README.md | 1 + doc/migration-v11-to-v12.md | 188 ++++++++++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 doc/migration-v11-to-v12.md diff --git a/Directory.Build.props b/Directory.Build.props index a797260..156a95e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ Ugo Lattanzi - 12.0.0 + 12.1.0 @@ -46,6 +46,15 @@ Features: Serializer packages (pick one): System.Text.Json, Newtonsoft, MemoryPack, MsgPack, Protobuf, ServiceStack, Utf8Json. +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) diff --git a/README.md b/README.md index c2fa1f2..2570c11 100644 --- a/README.md +++ b/README.md @@ -326,6 +326,7 @@ Full documentation is available in the [doc/](doc/) folder: - [Compression](doc/compressors.md) — GZip, Brotli, LZ4, Snappy, Zstandard **Advanced** +- [Migration Guide: v11 → v12](doc/migration-v11-to-v12.md) - [Logging & Diagnostics](doc/logging.md) - [Multiple Redis Servers](doc/multipleServers.md) - [Azure Managed Identity](doc/azure-managed-identity.md) diff --git a/doc/README.md b/doc/README.md index 842899a..11aee7c 100644 --- a/doc/README.md +++ b/doc/README.md @@ -40,6 +40,7 @@ ## Advanced +* [Migration Guide: v11 → v12](migration-v11-to-v12.md) * [Logging & Diagnostics](logging.md) * [Multiple Redis Servers](multipleServers.md) * [Azure Managed Identity](azure-managed-identity.md) diff --git a/doc/migration-v11-to-v12.md b/doc/migration-v11-to-v12.md new file mode 100644 index 0000000..22ae56c --- /dev/null +++ b/doc/migration-v11-to-v12.md @@ -0,0 +1,188 @@ +# Migration Guide: v11 to v12 + +This guide covers everything you need to know to upgrade from StackExchange.Redis.Extensions v11 to v12. + +## Quick Summary + +v12 is a major release with **no breaking changes to the public API**. All existing code continues to work. The upgrade brings new features, bug fixes, performance improvements, and 5 new NuGet packages for compression. + +## Step 1: Update NuGet Packages + +```bash +# Update core packages +dotnet add package StackExchange.Redis.Extensions.Core --version 12.1.0 +dotnet add package StackExchange.Redis.Extensions.AspNetCore --version 12.1.0 + +# Update your serializer (pick the one you use) +dotnet add package StackExchange.Redis.Extensions.System.Text.Json --version 12.1.0 +# or: Newtonsoft, MsgPack, Protobuf, MemoryPack, ServiceStack, Utf8Json + +# Optional: add compression (new in v12) +dotnet add package StackExchange.Redis.Extensions.Compression.LZ4 --version 12.1.0 +``` + +## Step 2: Verify Your Configuration + +### SyncTimeout Default Changed + +The default `SyncTimeout` changed from **1000ms to 5000ms** to match StackExchange.Redis defaults and the XML documentation. + +**If you were relying on the old 1000ms default** and want to keep it: +```csharp +config.SyncTimeout = 1000; +``` + +**If you had no explicit SyncTimeout**, your app now has a more generous timeout, which should reduce `RedisTimeoutException` occurrences. + +### Sentinel Users + +`CommandMap.Sentinel` is **no longer applied** to the master connection. In v11, Sentinel configuration incorrectly disabled data commands (GET, SET, EVAL, SCAN) on the resolved master. This is now fixed. + +**Action required:** If you had a workaround for this (e.g., `ExcludeCommands = null` or connection string overrides), you can remove it. + +### StackExchange.Redis Upgraded + +The SE.Redis dependency changed from `[2.8.*,3.0)` to `2.12.14`. This is a **pinned version** (no range). + +**Possible impact:** If you were using SE.Redis features that changed between 2.8 and 2.12, check the [SE.Redis release notes](https://github.com/StackExchange/StackExchange.Redis/releases). + +## Step 3: Take Advantage of New Features + +### .NET 10 Support + +v12 targets `netstandard2.1`, `net8.0`, `net9.0`, and `net10.0`. No action needed — the correct TFM is selected automatically. + +### Connection Pool Resilience + +`GetConnection()` now **skips disconnected connections** automatically. If all connections are down, the pool falls back to any connection (letting SE.Redis's internal reconnection handle recovery). A warning is logged (EventId 1003). + +**Action:** No code changes needed. This just works. + +### Pub/Sub Error Handling + +Subscription handler exceptions are now **logged** (EventId 4001) instead of being silently swallowed. To see these logs, ensure `RedisConfiguration.LoggerFactory` is set (automatic with `AddStackExchangeRedisExtensions`). + +**Action:** Check your logs for subscription handler errors that were previously hidden. + +### AddAllAsync with Expiry — Fixed + +In v11, `AddAllAsync` with expiry used a two-phase approach (MSET + separate EXPIREAT commands) that had a race condition. In v12, each key is set atomically with its expiry via `SET key value PX ` in a batch. + +**Action:** No code changes needed. Your data is now more reliable. + +### New: GeoSpatial API + +```csharp +await redis.GeoAddAsync("stores", 13.361389, 38.115556, "Palermo"); +var nearby = await redis.GeoSearchAsync("stores", 13.361389, 38.115556, + new GeoSearchCircle(200, GeoUnit.Kilometers)); +``` + +[Full documentation](geospatial.md) + +### New: Redis Streams + +```csharp +await redis.StreamAddAsync("orders", "payload", orderData); +var entries = await redis.StreamReadGroupAsync("orders", "processors", "worker-1", ">"); +await redis.StreamAcknowledgeAsync("orders", "processors", entries[0].Id.ToString()); +``` + +[Full documentation](streams.md) + +### New: Hash Field Expiry (Redis 7.4+) + +```csharp +await redis.HashSetWithExpiryAsync("user:1", "session", data, TimeSpan.FromMinutes(30)); +``` + +[Full documentation](hash-field-expiry.md) + +### New: VectorSet for AI/ML (Redis 8.0+) + +```csharp +await redis.VectorSetAddAsync("docs", VectorSetAddRequest.Member("doc-1", embedding)); +using var results = await redis.VectorSetSimilaritySearchAsync("docs", + VectorSetSimilaritySearchRequest.ByVector(queryEmb) with { Count = 5 }); +``` + +[Full documentation](vectorset.md) + +### New: Transparent Compression + +```csharp +services.AddStackExchangeRedisExtensions(config); +services.AddRedisCompression(); // one line! +``` + +**Warning:** Enabling compression on existing data makes old (uncompressed) values unreadable. Plan a migration strategy. + +[Full documentation](compressors.md) + +### New: Azure Managed Identity + +```csharp +config.ConfigurationOptionsAsyncHandler = async opts => +{ + await opts.ConfigureForAzureWithTokenCredentialAsync(new DefaultAzureCredential()); + return opts; +}; +``` + +[Full documentation](azure-managed-identity.md) + +### New: Configuration Properties + +| Property | Description | +|----------|-------------| +| `ClientName` | Set Redis connection client name | +| `KeepAlive` | Heartbeat interval (seconds). -1 = default, 0 = disabled | +| `CertificateSelection` | TLS client certificate selection callback | +| `ConfigurationOptionsAsyncHandler` | Async callback for custom ConfigurationOptions (e.g., Azure) | + +### New: Source-Generated Logging + +All logging now uses `[LoggerMessage]` attributes for zero-allocation performance. [See logging reference](logging.md) for EventId table. + +## Step 4: Test + +```bash +# Run tests against your codebase +dotnet test + +# If using Moq — consider switching to NSubstitute +# v12 replaced Moq internally due to the SponsorLink data collection incident +``` + +## Dependency Changes + +| Package | v11 | v12 | +|---------|-----|-----| +| StackExchange.Redis | [2.8.*,3.0) | 2.12.14 | +| Target Frameworks | netstandard2.1, net8.0, net9.0 | + net10.0 | +| Test Framework | Moq | NSubstitute | +| Analyzers | Roslynator 4.12, CodeAnalysis 3.11 | Roslynator 4.15, CodeAnalysis 5.3 | + +## New NuGet Packages (v12) + +| Package | Description | +|---------|-------------| +| `StackExchange.Redis.Extensions.Compression.LZ4` | LZ4 compression (fastest) | +| `StackExchange.Redis.Extensions.Compression.Snappier` | Snappy compression | +| `StackExchange.Redis.Extensions.Compression.ZstdSharp` | Zstandard compression | +| `StackExchange.Redis.Extensions.Compression.GZip` | GZip compression (no deps) | +| `StackExchange.Redis.Extensions.Compression.Brotli` | Brotli compression (no deps) | + +## FAQ + +**Q: Is v12 backward compatible with v11?** +A: Yes. No public API was removed or changed. All new features are additive. + +**Q: Do I need to flush Redis when upgrading?** +A: No. Existing data is fully compatible. Only enable compression if you understand the migration implications. + +**Q: Does v12 support .NET 6 or .NET 7?** +A: Not as explicit TFMs, but `netstandard2.1` covers .NET Core 3.0+ and .NET 5+. + +**Q: I was using the `nuget` branch to trigger publish. Is that still supported?** +A: No. Publishing is now manual via GitHub Actions workflow dispatch. The `nuget` branch has been deleted.