- 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
6.7 KiB
Operations Overview
This document covers running the server, graceful shutdown, connecting clients, and the test suite.
Running the Server
The host application is src/NATS.Server.Host. Run it with dotnet run:
# Default (port 4222, bind 0.0.0.0)
dotnet run --project src/NATS.Server.Host
# Custom port
dotnet run --project src/NATS.Server.Host -- -p 14222
# Custom bind address and name
dotnet run --project src/NATS.Server.Host -- -a 127.0.0.1 -n my-server
Arguments after -- are passed to the program. The three supported flags are -p/--port, -a/--addr, and -n/--name. See Configuration Overview for the full CLI reference.
On startup, the server logs the address it is listening on:
[14:32:01 INF] Listening on 0.0.0.0:4222
Full host setup
Program.cs initializes Serilog, parses CLI arguments, starts the server, and handles graceful shutdown:
using NATS.Server;
using Serilog;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.Enrich.FromLogContext()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
var options = new NatsOptions();
for (int i = 0; i < args.Length; i++)
{
switch (args[i])
{
case "-p" or "--port" when i + 1 < args.Length:
options.Port = int.Parse(args[++i]);
break;
case "-a" or "--addr" when i + 1 < args.Length:
options.Host = args[++i];
break;
case "-n" or "--name" when i + 1 < args.Length:
options.ServerName = args[++i];
break;
}
}
using var loggerFactory = new Serilog.Extensions.Logging.SerilogLoggerFactory(Log.Logger);
var server = new NatsServer(options, loggerFactory);
var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) =>
{
e.Cancel = true;
cts.Cancel();
};
try
{
await server.StartAsync(cts.Token);
}
catch (OperationCanceledException) { }
finally
{
Log.CloseAndFlush();
}
Graceful Shutdown
Pressing Ctrl+C triggers Console.CancelKeyPress. The handler sets e.Cancel = true — this prevents the process from terminating immediately — and calls cts.Cancel() to signal the CancellationToken passed to server.StartAsync.
NatsServer.StartAsync exits its accept loop on cancellation. In-flight client connections are left to drain naturally. After StartAsync returns (via OperationCanceledException which is caught), the finally block runs Log.CloseAndFlush() to ensure all buffered log output is written before the process exits.
Testing
Run the full test suite with:
dotnet test
The test project is at tests/NATS.Server.Tests/. It uses xUnit with Shouldly for assertions.
Test summary
69 tests across 6 test files:
| File | Tests | Coverage |
|---|---|---|
SubjectMatchTests.cs |
33 | Subject validation and wildcard matching |
SubListTests.cs |
12 | Trie insert, remove, match, queue groups, cache |
ParserTests.cs |
14 | All command types, split packets, case insensitivity |
ClientTests.cs |
2 | Socket-level INFO on connect, PING/PONG |
ServerTests.cs |
3 | End-to-end accept, pub/sub, wildcard delivery |
IntegrationTests.cs |
5 | NATS.Client.Core protocol compatibility |
Test categories
SubjectMatchTests — 33 [Theory] cases verifying SubjectMatch.IsValidSubject (16 cases), SubjectMatch.IsValidPublishSubject (6 cases), and SubjectMatch.MatchLiteral (11 cases). Covers empty strings, leading/trailing dots, embedded spaces, > in non-terminal position, and all wildcard combinations.
SubListTests — 12 [Fact] tests exercising the SubList trie directly: literal insert and match, empty result, * wildcard at various token levels, > wildcard, root >, multiple overlapping subscriptions, remove, queue group grouping, Count tracking, and cache invalidation after a wildcard insert.
ParserTests — 14 async [Fact] tests that write protocol bytes into a Pipe and assert on the resulting ParsedCommand list. Covers PING, PONG, CONNECT, SUB (with and without queue group), UNSUB (with and without max-messages), PUB (with payload, with reply-to, zero payload), HPUB (with header), INFO, multiple commands in a single buffer, and case-insensitive parsing.
ClientTests — 2 async [Fact] tests using a real loopback socket pair. Verifies that NatsClient sends an INFO frame immediately on connection, and that it responds PONG to a PING after CONNECT.
ServerTests — 3 async [Fact] tests that start NatsServer on a random port. Verifies INFO on connect, basic pub/sub delivery (MSG format), and wildcard subscription matching.
IntegrationTests — 5 async [Fact] tests using the official NATS.Client.Core v2.7.2 NuGet package. Verifies end-to-end protocol compatibility with a real NATS client library: basic pub/sub, * wildcard delivery, > wildcard delivery, fan-out to two subscribers, and PingAsync.
Integration tests use NullLoggerFactory.Instance for the server so test output is not cluttered with server logs.
Connecting with Standard NATS Clients
The server speaks the standard NATS wire protocol. Any NATS client library can connect to it.
# Using the nats CLI tool
nats sub "test.>" --server nats://127.0.0.1:4222
nats pub test.hello "world" --server nats://127.0.0.1:4222
From .NET, using NATS.Client.Core:
var opts = new NatsOpts { Url = "nats://127.0.0.1:4222" };
await using var conn = new NatsConnection(opts);
await conn.ConnectAsync();
await conn.PublishAsync("test.hello", "world");
Go Reference Server
The Go reference implementation can be built from golang/nats-server/ for comparison testing:
# Build
cd golang/nats-server && go build
# Run on default port
./nats-server
# Run on custom port
./nats-server -p 14222
The Go server is useful for verifying that the .NET port produces identical protocol output and behaves the same way under edge-case client interactions.
Current Limitations
The following features present in the Go reference server are not yet ported:
- Authentication — no username/password, token, NKey, or JWT support
- Clustering — no routes, gateways, or leaf nodes
- JetStream — no persistent streaming, streams, consumers, or RAFT
- Monitoring — no HTTP endpoints (
/varz,/connz,/healthz, etc.) - TLS — all connections are plaintext
- WebSocket — no WebSocket transport