# NATS .NET Base Server Design **Date:** 2026-02-22 **Scope:** Minimal single-node NATS server — pub/sub, queue groups, wildcards. No auth, clustering, JetStream, or monitoring. **Target:** .NET 10, C# **Approach:** Bottom-up (Sublist → Parser → Client → Server) ## Decisions - **Framework:** .NET 10 - **I/O:** System.IO.Pipelines for zero-copy protocol parsing - **Structure:** Library (NATS.Server) + Host (NATS.Server.Host) + Tests (NATS.Server.Tests) - **Test framework:** xUnit - **Queue group selection:** Round-robin with atomic counter ## Solution Layout ``` natsdotnet/ ├── NatsDotNet.sln ├── src/ │ ├── NATS.Server/ # Core library │ │ ├── NatsServer.cs # Server orchestrator │ │ ├── NatsClient.cs # Client connection │ │ ├── NatsOptions.cs # Configuration │ │ ├── Protocol/ │ │ │ ├── NatsProtocol.cs # Constants, op codes │ │ │ └── NatsParser.cs # Pipeline-based parser │ │ └── Subscriptions/ │ │ ├── Subscription.cs │ │ ├── SubjectMatch.cs │ │ └── SubList.cs # Trie + cache │ └── NATS.Server.Host/ # Console app │ └── Program.cs └── tests/ └── NATS.Server.Tests/ ``` ## Component Designs ### 1. SubList (subject matching trie) Reference: `golang/nats-server/server/sublist.go` **Structures:** - `SubList` — root trie + cache (`Dictionary`) + `ReaderWriterLockSlim` + atomic `genId` - `TrieLevel` — `Dictionary` for literal tokens, nullable `pwc` (`*`) and `fwc` (`>`) pointers - `TrieNode` — optional `next: TrieLevel`, `HashSet` for plain subs, `Dictionary>` for queue subs - `Subscription` — client ref, subject, queue, sid, atomic message count, max messages - `SubListResult` — `Subscription[]` plain subs, `Subscription[][]` queue subs (grouped) **Operations:** - `Insert(Subscription)` — walk trie by `.`-split tokens, create nodes, add sub, increment genId (clears cache) - `Remove(Subscription)` — reverse, prune empty nodes, increment genId - `Match(ReadOnlySpan)` — cache check (convert to string key on miss), walk trie collecting matches from literal + `*` + `>` nodes - Thread safety: read lock for Match, write lock for Insert/Remove ### 2. Protocol Parser Reference: `golang/nats-server/server/parser.go` Operates on `PipeReader`. Scans for `\r\n` to delimit control lines. Identifies command by first bytes. For `PUB`/`HPUB`, reads control line then exactly `size` bytes of payload + `\r\n`. **Commands (base server):** | Command | Direction | Control line format | |---------|-----------|-------------------| | INFO | S→C | `INFO {json}\r\n` | | CONNECT | C→S | `CONNECT {json}\r\n` | | PUB | C→S | `PUB subject [reply] size\r\n[payload]\r\n` | | HPUB | C→S | `HPUB subject [reply] hdr_size total_size\r\n[hdrs+payload]\r\n` | | SUB | C→S | `SUB subject [queue] sid\r\n` | | UNSUB | C→S | `UNSUB sid [max_msgs]\r\n` | | MSG | S→C | `MSG subject sid [reply] size\r\n[payload]\r\n` | | HMSG | S→C | `HMSG subject sid [reply] hdr_size total_size\r\n[hdrs+payload]\r\n` | | PING/PONG | Both | `PING\r\n` / `PONG\r\n` | | +OK / -ERR | S→C | `+OK\r\n` / `-ERR 'msg'\r\n` | **Design choices:** - Parser returns parsed command structs, does not dispatch - No string allocation on hot path — subject matching uses `Span` - Max control line: 4096 bytes, max payload: 1MB default (configurable) ### 3. NatsClient (connection handler) Reference: `golang/nats-server/server/client.go` **Fields:** unique id, Socket, IDuplexPipe, back-ref to server, subs dict (sid→Subscription), ClientOptions (from CONNECT json), stats (Interlocked counters), SemaphoreSlim for write serialization. **Lifecycle:** 1. Server accepts socket, creates NatsClient 2. `RunAsync(CancellationToken)`: send INFO → read loop (parse + dispatch commands) 3. Dispatch: CONNECT→validate, SUB→insert into SubList, UNSUB→remove, PUB→`server.ProcessMessage()`, PING→PONG 4. On disconnect: remove all subs, remove from server clients dict **Writing:** `SendMessageAsync()` formats MSG/HMSG, acquires write lock, writes to PipeWriter, flushes. ### 4. NatsServer (orchestrator) Reference: `golang/nats-server/server/server.go` **Fields:** NatsOptions, TCP listener Socket, `ConcurrentDictionary`, SubList, atomic next client ID, ServerInfo struct, CancellationTokenSource. **Core methods:** - `StartAsync()` — bind, accept loop - `ProcessMessage(subject, reply, headers, payload, sender)` — SubList.Match → deliver to plain subs + pick one per queue group (round-robin) - `RemoveClient(client)` — cleanup subs and client dict - `Shutdown()` — cancel, close listener, drain ### 5. NatsOptions Reference: `golang/nats-server/server/opts.go` Minimal for base: Host, Port (default 4222), ServerName, MaxPayload (1MB), MaxControlLine (4096), MaxConnections, PingInterval, MaxPingsOut. ## Testing Strategy **Unit tests:** - SubList: insert/remove/match with literals, `*`, `>`, queue groups, cache invalidation. Port Go test cases. - Parser: each command type, malformed input, partial reads, boundary sizes. - Client protocol: CONNECT handshake, SUB/UNSUB lifecycle, PUB delivery, PING/PONG via loopback sockets. **Integration tests (using NATS.Client.Core NuGet):** - Basic pub/sub, wildcard matching, queue group distribution, fan-out, UNSUB + auto-unsub, PING/PONG. ## Module Boundaries (future) These are explicitly excluded from this design and will be added as separate modules later: | Module | Key files in Go reference | |--------|--------------------------| | Authentication | auth.go, auth_callout.go, nkey.go, jwt.go | | Monitoring HTTP | monitor.go | | Clustering/Routes | route.go | | Gateways | gateway.go | | Leaf Nodes | leafnode.go | | JetStream | jetstream.go, stream.go, consumer.go, filestore.go, memstore.go, raft.go | | WebSocket | websocket.go | | MQTT | mqtt.go | | TLS | TLS config in opts.go, various TLS setup in server.go |