Guidance for AI coding agents working in this repository. Claude Code reads this via a CLAUDE.md that imports it.
StackExchange.Redis — a high-performance .NET client for RESP servers (Redis, Valkey, Garnet, Azure Managed Redis, AWS ElastiCache, etc.). This is the v3 line (VersionPrefix 3.0, current branch work on marc/agents), whose defining change is that the low-level IO core has been extracted into a separate RESPite library that StackExchange.Redis now sits on top of.
src/StackExchange.Redis/— the client library (the NuGet package). Public surface:ConnectionMultiplexer,IDatabase/IDatabaseAsync,IServer,ISubscriber,ITransaction/IBatch,ConfigurationOptions,RedisValue/RedisKey/RedisResult.src/RESPite/— standalone low-level RESP protocol library (separate package,Marc Gravellcopyright). Owns wire-level parsing/writing:RespReader,RespFrameScanner,RespPrefix, buffer pooling (CycleBuffer,MemoryTrackedPool). StackExchange.Redis depends on it viaProjectReference. RESPite has no dependency on StackExchange.Redis.eng/StackExchange.Redis.Build/— a Roslyn analyzer/source-generator project that every other project referencesOutputItemType="Analyzer". It generates code (e.g.AsciiHashGenerator) and enforces project-specific rules. Not shipped.tests/—StackExchange.Redis.Tests(xUnit v3, the main integration suite),RESPite.Tests,*.Benchmarks(BenchmarkDotNet), andRedisConfigs(server configs + docker compose, see Testing).toys/— runnable samples and an in-process RESP server (StackExchange.Redis.Server, used by tests as a managed fake server), Kestrel-hosted server, console tools.docs/— published documentation site source (markdown).docs/ReleaseNotes.mdis the changelog (frozen at 3.0 — from v3 onward release notes live in GitHub Releases).
Build.csproj is a Microsoft.Build.Traversal project that references everything under eng/src/tests/toys — build/test/pack it to act on the whole repo. StackExchange.Redis.slnx is the IDE solution (XML SLNX format, not classic .sln).
# Build everything (CI uses Release)
dotnet build Build.csproj -c Release /p:CI=true
# Full local build + start servers + run tests (Windows-oriented, also build.cmd)
pwsh ./build.ps1 -StartServers
# Start the Redis test servers in the background (preferred; see "Testing topology")
docker compose --file tests/RedisConfigs/docker-compose.yml up -d --wait
# Run the main test suite against one target framework (fastest inner loop)
dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release -f net10.0
# Run a single test class / method (xUnit v3 + Microsoft.Testing.Platform)
dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -f net10.0 --filter "FullyQualifiedName~ClassName.MethodName"
# Pack the library
dotnet pack src/StackExchange.Redis/StackExchange.Redis.csproj --no-build -c Release /p:Packing=true- SDK is pinned (
global.json,allowPrerelease: false); CI installs the 6/8/10 runtimes.LangVersionis 14. TreatWarningsAsErrors=trueeverywhere andFeatures=strict— warnings fail the build. Analyzers (StyleCop + the customenganalyzer + PublicApiAnalyzers) run as part of the build.- Analyzers run on one TFM per project — the newest it builds (see
Directory.Build.targets), because they cost ~40% of a clean build otherwise and every rule is TFM-agnostic. Source generators still run on every TFM. If you touch code inside a down-level#if, build it with/p:RunAnalyzers=trueto get the full per-TFM sweep. StyleCop applies tosrc/only, nottests/ortoys/. - Library multi-targets
net461;netstandard2.0;net472;net6.0;net8.0;net10.0. Conditional compile symbols:VECTOR_SAFE(all but net461),UNIX_SOCKET(net6.0+). The test project targetsnet481;net8.0;net10.0;BUILD_CURRENTis defined on the newest TFM (disables some parallelism for brittle tests).
Both shipped libraries use Microsoft.CodeAnalysis.PublicApiAnalyzers. Any change to the public surface fails the build until you update the API text files:
src/StackExchange.Redis/PublicAPI/PublicAPI.{Shipped,Unshipped}.txt(and thenet6.0/subfolder for APIs that only exist on newer TFMs — each folder is effectivelyNET_X_Y_OR_GREATER).src/RESPite/PublicAPI/...likewise (withnet8.0/).
Add new members to PublicAPI.Unshipped.txt. The build error message tells you the exact line to add.
This library is heavily used and referenced across the .NET ecosystem, so hard breaks to shipped public APIs are extremely discouraged — especially binary breaks that surface as MissingMethodException/MissingFieldException at runtime for callers compiled against an older version. Note that source-compatible changes can still be binary breaks: adding an optional parameter to an existing method changes its signature and breaks already-compiled callers, so do not do it. The same applies to changing parameter/return types, renaming members, or removing them.
Prefer additive, non-breaking patterns instead:
- Add a new overload rather than modifying an existing method's signature; use
[OverloadResolutionPriority(...)]to steer the compiler toward the preferred overload when several would otherwise be ambiguous. - Deprecate, don't delete: mark the old member
[Obsolete(...)](keeping it functional) and point callers at the replacement. - When unsure whether a change is breaking, treat it as breaking and reach for an overload — or raise it for human review.
Newer features (especially pre-release server APIs) are typically gated behind [Experimental(...)] diagnostic IDs defined in src/RESPite/Shared/Experiments.cs (SER001–SER006, e.g. Respite = "SER004", version-gated server features Server_8_4/8_6/8_8). These IDs are in the root NoWarn list so consuming them internally doesn't error; docs live under docs/exp/.
Request flow, roughly outer → inner:
ConnectionMultiplexer(split across manyConnectionMultiplexer.*.cspartials) is the root object — one per logical Redis deployment, meant to be shared/long-lived. It owns endpoints, configuration, pub/sub, sentinel logic, and server selection.IDatabase/RedisDatabase(RedisDatabase.cs, ~6k lines) is the command surface. Each command builds aMessageand hands it to the multiplexer with aResultProcessor<T>that knows how to parse the reply into the typed result.Message.csandResultProcessor.csare the two hubs to understand command implementation — to add/modify a command, you create the message + pick/extend a result processor.ServerEndPointrepresents one physical server;PhysicalBridgemanages the queue/backlog and connection lifecycle for a server;PhysicalConnectionis the actual socket + read/write loop. This is where pipelining and the backlog policy live (seedocs/PipelinesMultiplexers.md).- RESPite does the byte-level RESP framing beneath
PhysicalConnection— scanning frames off the buffer (RespFrameScanner), reading values (RespReader, aref structwith many.cspartials), and pooled buffers.
Cross-cutting: CommandMap (command renaming/disabling per server type), ServerType/cluster slot routing (ClusterConfiguration, ServerSelectionStrategy), CommandFlags (sync/async, fire-and-forget, replica preference), keyspace isolation (KeyspaceIsolation/), profiling (Profiling/), and maintenance events (Maintenance/). RESP3 push/attribute support is reflected in both the reader and result processors.
Partial-class file naming is heavily used: Foo.cs + Foo.Bar.cs are one type (the csproj wires DependentUpon). When editing a type, check for sibling Foo.*.cs files.
Many tests are pure unit tests, or run against the in-process managed test server (toys/StackExchange.Redis.Server) and need no external Redis at all. The rest are integration tests that talk to a real server.
The integration suite needs a full local Redis topology, not a single server. Bring it up with docker compose:
cd tests/RedisConfigs && docker compose up -d --waitExpected servers (defaults in tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs):
6379primary,6380replica (standalone tests use6379,6380)6382/6383failover pair,6381/6384secure/TLS7000-7005cluster nodes7010/7011+26379-26381sentinel
Tests skip as inconclusive when their required server is absent (e.g. cluster tests skip with "Unable to connect to server"). Override hosts/ports for local runs with a tests/StackExchange.Redis.Tests/TestConfig.json (gitignored). A stray container squatting on 6379 is a common failure: it makes the primary reachable but leaves no replica/cluster, so replica/cluster tests fail or skip — clear it before bringing the compose up.
To probe these servers ad hoc, the local user may have resp-cli installed — a dotnet global tool that is functionally similar to redis-cli (same basic flags: -p, -a, -n, --tls, -3). Prefer resp-cli when it's available; fall back to redis-cli otherwise.
Repo-specific Agent Skills (the portable SKILL.md open standard) live under .claude/skills/:
implement-resp-command— add a new RESP command to StackExchange.Redis end-to-end (enum, interfaces,RedisDatabase,ResultProcessor, public-API tracking, and the ResultProcessor + RoundTrip unit tests).summarize-database— profile a live (often production) RESP database by sampling: discover key patterns, where data lives by count and size, and what the values are. Read-only.
That path is where Claude Code discovers them; the files themselves are tool-agnostic, so if your agent reads skills from a different directory (Codex uses .agents/skills/, etc.), point it at this folder or copy the skill across.
Be consise and direct where possible, but with as much detail as is necessary; no need for flattery, and feel free to challenge anything the user says: users hallucinate too.
- Code style is enforced via
.editorconfig+Shared.ruleset+ StyleCop (StyleCop insrc/only); 4-space indent, BOM + final newline on.cs,System.*usings first, no redundantthis.. Build will fail on violations. Match the surrounding style intests//toys/too — the rules are simply not enforced there. InternalsVisibleToexposes internals to the test/benchmark/server projects, so tests reach into internal types directly.docs/markdown is the user-facing documentation; update it for user-visible behavior changes.