- Add Microsoft.Extensions.Logging + Serilog to NatsServer and NatsClient - Convert all test assertions from xUnit Assert to Shouldly - Add NSubstitute package for future mocking needs - Introduce Central Package Management via Directory.Packages.props - Add documentation_rules.md with style guide, generation/update rules, component map - Generate 10 documentation files across 5 component folders (GettingStarted, Protocol, Subscriptions, Server, Configuration/Operations) - Update CLAUDE.md with logging, testing, porting, agent model, CPM, and documentation guidance
11 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
This project ports the NATS server from Go to .NET 10 / C#. The Go reference implementation lives in golang/nats-server/. The .NET port lives at the repository root.
NATS is a high-performance publish-subscribe messaging system. It supports wildcards (* single token, > multi-token), queue groups for load balancing, request-reply, clustering (full-mesh routes, gateways, leaf nodes), and persistent streaming via JetStream.
Build & Test Commands
The solution file is NatsDotNet.slnx.
# Build the solution
dotnet build
# Run all tests
dotnet test
# Run tests with verbose output
dotnet test -v normal
# Run a single test project
dotnet test tests/NATS.Server.Tests
# Run a specific test by name
dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~TestName"
# Run the NATS server (default port 4222)
dotnet run --project src/NATS.Server.Host
# Run the NATS server on a custom port
dotnet run --project src/NATS.Server.Host -- -p 14222
# Clean and rebuild
dotnet clean && dotnet build
.NET Project Structure
NatsDotNet.slnx # Solution file
src/
NATS.Server/ # Core server library
NatsServer.cs # Server: listener, accept loop, shutdown
NatsClient.cs # Per-connection client: read/write loops, sub tracking
NatsOptions.cs # Server configuration (port, host, etc.)
Protocol/
NatsParser.cs # Protocol state machine (PUB, SUB, UNSUB, etc.)
NatsProtocol.cs # Wire-level protocol writing (INFO, MSG, PING/PONG)
Subscriptions/
SubjectMatch.cs # Subject validation and wildcard matching
SubList.cs # Trie-based subscription list with caching
SubListResult.cs # Match result container (plain subs + queue groups)
Subscription.cs # Subscription model (subject, sid, queue, client)
NATS.Server.Host/ # Executable host app
Program.cs # Entry point, CLI arg parsing (-p port)
tests/
NATS.Server.Tests/ # xUnit test project
ParserTests.cs # Protocol parser tests
SubjectMatchTests.cs # Subject validation & matching tests
SubListTests.cs # Subscription list trie tests
ClientTests.cs # Client-level protocol tests
ServerTests.cs # Server pubsub/wildcard tests
IntegrationTests.cs # End-to-end tests using NATS.Client.Core NuGet
Go Reference Commands
# Build the Go reference server
cd golang/nats-server && go build
# Run Go tests for a specific area
cd golang/nats-server && go test -v -run TestName ./server/ -count=1 -timeout=30m
# Run all Go server tests (slow, ~30min)
cd golang/nats-server && go test -v ./server/ -count=1 -timeout=30m
Architecture: NATS Server (Reference)
The Go source in golang/nats-server/server/ is the authoritative reference. Key files by subsystem:
Core Message Path
server.go— Server struct, startup lifecycle (NewServer→Run→WaitForShutdown), listener managementclient.go(6700 lines) — Connection handling,readLoop/writeLoopgoroutines, per-client subscription tracking, dynamic buffer sizing (512→65536 bytes), client types:CLIENT,ROUTER,GATEWAY,LEAF,SYSTEMparser.go— Protocol state machine. Text protocol:PUB,SUB,UNSUB,CONNECT,INFO,PING/PONG,MSG. Extended:HPUB/HMSG(headers),RPUB/RMSG(routes). Control line limit: 4096 bytes. Default max payload: 1MB.sublist.go— Trie-based subject matcher with wildcard support. Nodes havepsubs(plain),qsubs(queue groups), special pointers for*and>wildcards. Results are cached with atomic generation IDs for invalidation.
Authentication & Accounts
auth.go— Auth mechanisms: username/password, token, NKeys (Ed25519), JWT, external auth callout, LDAPaccounts.go(137KB) — Multi-tenant account isolation. Each account has its ownSublist, client set, and subject namespace. Supports exports/imports between accounts, service latency tracking.jwt.go,nkey.go— JWT claims parsing and NKey validation
Clustering
route.go— Full-mesh cluster routes. Route pooling (default 3 connections per peer). Account-specific dedicated routes. Protocol:RS+/RS-for subscribe propagation,RMSGfor routed messages.gateway.go(103KB) — Inter-cluster bridges. Interest-only mode optimizes traffic. Reply subject mapping (_GR_.prefix) avoids cross-cluster conflicts.leafnode.go— Hub-and-spoke topology for edge deployments. Only subscribed subjects shared with hub. Loop detection via$LDS.prefix.
JetStream (Persistence)
jetstream.go— Orchestration, API subject handlers ($JS.API.*)stream.go(8000 lines) — Stream lifecycle, retention policies (Limits, Interest, WorkQueue), subject transforms, mirroring/sourcingconsumer.go— Stateful readers. Push vs pull delivery. Ack policies: None, All, Explicit. Redelivery tracking, priority groups.filestore.go(337KB) — Block-based persistent storage with S2 compression, encryption (ChaCha20/AES-GCM), indexingmemstore.go— In-memory storage with hash-wheel TTL expirationraft.go— RAFT consensus for clustered JetStream. Meta-cluster for metadata, per-stream/consumer RAFT groups.
Configuration & Monitoring
opts.go— CLI flags + config file loading. CLI overrides config. Supports hot reload on signal.monitor.go— HTTP endpoints:/varz,/connz,/routez,/gatewayz,/jsz,/healthzconf/— Config file parser (custom format with includes)
Internal Data Structures
server/avl/— AVL tree for sparse sequence sets (ack tracking)server/stree/— Subject tree for per-subject state in streamsserver/gsl/— Generic subject list, optimized trieserver/thw/— Time hash wheel for efficient TTL expiration
Key Porting Considerations
Concurrency model: Go uses goroutines (one per connection readLoop + writeLoop). Map to async/await with Task-based I/O. Use Channel<T> or Pipe for producer-consumer patterns where Go uses channels.
Locking: Go sync.RWMutex maps to ReaderWriterLockSlim. Go sync.Map maps to ConcurrentDictionary. Go atomic operations map to Interlocked or volatile.
Subject matching: The Sublist trie is performance-critical. Every published message triggers a Match() call. Cache invalidation uses atomic generation counters.
Protocol parsing: The parser is a byte-by-byte state machine. In .NET, use System.IO.Pipelines for zero-copy parsing with ReadOnlySequence<byte>.
Buffer management: Go uses []byte slices with pooling. Map to ArrayPool<byte> and Memory<T>/Span<T>.
Compression: NATS uses S2 (Snappy variant) for route/gateway compression. Use an equivalent .NET S2 library or IronSnappy.
Ports: Client=4222, Cluster=6222, Monitoring=8222, Leaf=5222, Gateway=7222.
Message Flow Summary
Client PUB → parser → permission check → Sublist.Match() →
├─ Local subscribers: MSG to each (queue subs: pick one per group)
├─ Cluster routes: RMSG to peers (who deliver to their locals)
├─ Gateways: forward to interested remote clusters
└─ JetStream: if subject matches a stream, store + deliver to consumers
NuGet Package Management
This solution uses Central Package Management (CPM) via Directory.Packages.props at the repo root. All package versions are defined centrally there.
- In
.csprojfiles, use<PackageReference Include="Foo" />without aVersionattribute - To add a new package: add a
<PackageVersion>entry inDirectory.Packages.props, then reference it without version in the project's csproj - To update a version: change it only in
Directory.Packages.props— all projects pick it up automatically - Never specify
Versionon<PackageReference>in individual csproj files
Logging
Use Microsoft.Extensions.Logging (ILogger<T>) for all logging throughout the server. Wire up Serilog as the logging provider in the host application.
- Inject
ILogger<T>via constructor in all components (NatsServer, NatsClient, etc.) - Use Serilog.Context.LogContext to push contextual properties (client ID, remote endpoint, subscription subject) so they appear on all log entries within that scope
- Use structured logging with message templates:
logger.LogInformation("Client {ClientId} subscribed to {Subject}", id, subject)— never string interpolation - Log levels:
Tracefor protocol bytes,Debugfor per-message flow,Informationfor lifecycle events (connect/disconnect),Warningfor protocol violations,Errorfor unexpected failures
Testing
- xUnit 3 for test framework
- Shouldly for assertions — use
value.ShouldBe(expected),action.ShouldThrow<T>(), etc. Do NOT useAssert.*from xUnit - NSubstitute for mocking/substitution when needed
- Do NOT use FluentAssertions or Moq — these are explicitly excluded
- Test project uses global
using Shouldly;
Porting Guidelines
- Use modern .NET 10 / C# 14 best practices (primary constructors, collection expressions,
fieldkeyword where stable, file-scoped namespaces, raw string literals, etc.) - Prefer
readonly record structfor small value types over mutable structs - Use
requiredproperties andinitsetters for initialization-only state - Use pattern matching and switch expressions where they improve clarity
- Prefer
System.Text.Jsonsource generators for JSON serialization - Use
ValueTaskwhere appropriate for hot-path async methods
Agent Model Guidance
- Sonnet (
model: "sonnet") — use for simpler implementation tasks: straightforward file modifications, adding packages, converting assertions, boilerplate code - Opus (default) — use for complex tasks, architectural decisions, design work, tricky protocol logic, and code review
- Parallel subagents — use where tasks are independent and don't touch the same files (e.g., converting test files in parallel, adding packages while updating docs)
Documentation
Follow the documentation rules in documentation_rules.md for all project documentation. Key points:
- Documentation lives in
Documentation/with component subfolders (Protocol, Subscriptions, Server, Configuration, Operations) - Use
PascalCase.mdfile names, always specify language on code blocks, use real code snippets (not invented examples) - Update documentation when code changes — see the trigger rules and component map in the rules file
- Technical and direct tone, explain "why" not just "what", present tense
Conventions
- Reference the Go implementation file and line when porting a subsystem
- Maintain protocol compatibility — the .NET server must interoperate with existing NATS clients and Go servers in a cluster
- Use the same configuration file format as the Go server (parsed by
conf/package) - Match the Go server's monitoring JSON response shapes for tooling compatibility