diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/ClosedRingBuffer.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/ClosedRingBuffer.cs
new file mode 100644
index 0000000..09109be
--- /dev/null
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/ClosedRingBuffer.cs
@@ -0,0 +1,118 @@
+// Copyright 2018-2025 The NATS Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// Adapted from server/ring.go in the NATS server Go source.
+
+namespace ZB.MOM.NatsNet.Server.Internal;
+
+// -------------------------------------------------------------------------
+// Placeholder types — will be populated in session 08 (Client) and
+// session 12 (Monitor/Events).
+// -------------------------------------------------------------------------
+
+///
+/// Subset of client connection info exposed on the monitoring endpoint (/connz).
+/// Full implementation is in session 12 (monitor.go).
+///
+public class ConnInfo { }
+
+///
+/// Subscription detail for the monitoring endpoint.
+/// Full implementation is in session 12 (monitor.go).
+///
+public class SubDetail { }
+
+// -------------------------------------------------------------------------
+// ClosedClient / ClosedRingBuffer (ring.go)
+// -------------------------------------------------------------------------
+
+///
+/// Wraps connection info with optional items for the /connz endpoint.
+/// Mirrors closedClient in server/ring.go.
+///
+public sealed class ClosedClient
+{
+ public ConnInfo Info { get; init; } = new();
+ public IReadOnlyList Subs { get; init; } = [];
+ public string User { get; init; } = string.Empty;
+ public string Account { get; init; } = string.Empty;
+}
+
+///
+/// Fixed-size ring buffer that retains the most recent closed connections,
+/// evicting the oldest when full.
+/// Mirrors closedRingBuffer in server/ring.go.
+///
+public sealed class ClosedRingBuffer
+{
+ private ulong _total;
+ private readonly ClosedClient?[] _conns;
+
+ /// Creates a ring buffer that holds at most entries.
+ public ClosedRingBuffer(int max)
+ {
+ _conns = new ClosedClient?[max];
+ }
+
+ ///
+ /// Appends a closed connection, evicting the oldest if the buffer is full.
+ /// Mirrors closedRingBuffer.append.
+ ///
+ public void Append(ClosedClient cc)
+ {
+ _conns[Next()] = cc;
+ _total++;
+ }
+
+ // Index of the slot to write next — wraps around.
+ private int Next() => (int)(_total % (ulong)_conns.Length);
+
+ ///
+ /// Returns the number of entries currently stored (≤ capacity).
+ /// Mirrors closedRingBuffer.len.
+ ///
+ public int Len() =>
+ _total > (ulong)_conns.Length ? _conns.Length : (int)_total;
+
+ ///
+ /// Returns the total number of connections ever appended (not capped).
+ /// Mirrors closedRingBuffer.totalConns.
+ ///
+ public ulong TotalConns() => _total;
+
+ ///
+ /// Returns a chronologically ordered copy of the stored closed connections.
+ /// The caller may freely modify the returned array.
+ /// Mirrors closedRingBuffer.closedClients.
+ ///
+ public ClosedClient?[] ClosedClients()
+ {
+ var len = Len();
+ var dup = new ClosedClient?[len];
+ var head = Next();
+
+ if (_total <= (ulong)_conns.Length || head == 0)
+ {
+ Array.Copy(_conns, dup, len);
+ }
+ else
+ {
+ var fp = _conns.AsSpan(head); // oldest … end of array
+ var sp = _conns.AsSpan(0, head); // wrap-around … newest
+ fp.CopyTo(dup.AsSpan());
+ sp.CopyTo(dup.AsSpan(fp.Length));
+ }
+
+ return dup;
+ }
+}
diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/DataStructures/StreamDeletionMeta.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/DataStructures/StreamDeletionMeta.cs
new file mode 100644
index 0000000..4e7af96
--- /dev/null
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/DataStructures/StreamDeletionMeta.cs
@@ -0,0 +1,121 @@
+// Copyright 2025 The NATS Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// Adapted from server/sdm.go in the NATS server Go source.
+
+namespace ZB.MOM.NatsNet.Server.Internal.DataStructures;
+
+///
+/// Per-sequence data tracked by .
+/// Mirrors SDMBySeq in server/sdm.go.
+///
+public readonly struct SdmBySeq
+{
+ /// Whether this sequence was the last message for its subject.
+ public bool Last { get; init; }
+
+ /// Timestamp (nanoseconds UTC) when the removal/SDM was last proposed.
+ public long Ts { get; init; }
+}
+
+///
+/// Tracks pending subject delete markers (SDMs) and message removals for a stream.
+/// Used by JetStream cluster consensus to avoid redundant proposals.
+/// Mirrors SDMMeta in server/sdm.go.
+///
+public sealed class StreamDeletionMeta
+{
+ // Per-subject pending-count totals.
+ private readonly Dictionary _totals = new(1);
+
+ // Per-sequence data keyed by sequence number.
+ private readonly Dictionary _pending = new(1);
+
+ // -------------------------------------------------------------------------
+ // Header constants (forward-declared; populated in session 19 — JetStream).
+ // isSubjectDeleteMarker checks these header keys.
+ // -------------------------------------------------------------------------
+
+ // Mirrors JSMarkerReason header key (defined in jetstream.go).
+ internal const string HeaderJsMarkerReason = "Nats-Marker-Reason";
+
+ // Mirrors KVOperation header key (defined in jetstream.go).
+ internal const string HeaderKvOperation = "KV-Operation";
+
+ // Mirrors KVOperationValuePurge (defined in jetstream.go).
+ internal const string KvOperationValuePurge = "PURGE";
+
+ ///
+ /// Returns true when the given header block contains a subject delete marker
+ /// (either a JetStream marker or a KV purge operation).
+ /// Mirrors isSubjectDeleteMarker in server/sdm.go.
+ ///
+ public static bool IsSubjectDeleteMarker(ReadOnlySpan hdr)
+ {
+ // Simplified header scan: checks whether JSMarkerReason key is present
+ // or whether KV-Operation equals "PURGE".
+ // Full implementation depends on SliceHeader from session 08 (client.go).
+ // Until then this provides the correct contract.
+ var text = System.Text.Encoding.UTF8.GetString(hdr);
+ if (text.Contains(HeaderJsMarkerReason))
+ return true;
+ if (text.Contains($"{HeaderKvOperation}: {KvOperationValuePurge}"))
+ return true;
+ return false;
+ }
+
+ ///
+ /// Clears all tracked data.
+ /// Mirrors SDMMeta.empty.
+ ///
+ public void Empty()
+ {
+ _totals.Clear();
+ _pending.Clear();
+ }
+
+ ///
+ /// Tracks as pending and returns whether it was
+ /// the last message for its subject. If the sequence is already tracked
+ /// the existing Last value is returned without modification.
+ /// Mirrors SDMMeta.trackPending.
+ ///
+ public bool TrackPending(ulong seq, string subj, bool last)
+ {
+ if (_pending.TryGetValue(seq, out var p))
+ return p.Last;
+
+ _pending[seq] = new SdmBySeq { Last = last, Ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L };
+ _totals[subj] = _totals.TryGetValue(subj, out var cnt) ? cnt + 1 : 1;
+ return last;
+ }
+
+ ///
+ /// Removes and decrements the pending count for
+ /// , deleting the subject entry when it reaches zero.
+ /// Mirrors SDMMeta.removeSeqAndSubject.
+ ///
+ public void RemoveSeqAndSubject(ulong seq, string subj)
+ {
+ if (!_pending.Remove(seq))
+ return;
+
+ if (_totals.TryGetValue(subj, out var msgs))
+ {
+ if (msgs <= 1)
+ _totals.Remove(subj);
+ else
+ _totals[subj] = msgs - 1;
+ }
+ }
+}
diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/ProtoWire.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/ProtoWire.cs
new file mode 100644
index 0000000..22eaff2
--- /dev/null
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/ProtoWire.cs
@@ -0,0 +1,284 @@
+// Copyright 2024 The NATS Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// Adapted from server/proto.go in the NATS server Go source.
+// Inspired by https://github.com/protocolbuffers/protobuf-go/blob/master/encoding/protowire/wire.go
+
+namespace ZB.MOM.NatsNet.Server.Internal;
+
+///
+/// Low-level protobuf wire format helpers used internally for JetStream API encoding.
+/// Mirrors server/proto.go.
+///
+public static class ProtoWire
+{
+ private static readonly Exception ErrInsufficient =
+ new InvalidOperationException("insufficient data to read a value");
+
+ private static readonly Exception ErrOverflow =
+ new InvalidOperationException("too much data for a value");
+
+ private static readonly Exception ErrInvalidFieldNumber =
+ new InvalidOperationException("invalid field number");
+
+ // -------------------------------------------------------------------------
+ // Field scanning
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Reads a complete protobuf field (tag + value) from
+ /// and returns the field number, wire type, and total byte size consumed.
+ /// Mirrors protoScanField.
+ ///
+ public static (int num, int typ, int size, Exception? err) ScanField(ReadOnlySpan b)
+ {
+ var (num, typ, sizeTag, err) = ScanTag(b);
+ if (err != null) return (0, 0, 0, err);
+
+ var (sizeValue, err2) = ScanFieldValue(typ, b[sizeTag..]);
+ if (err2 != null) return (0, 0, 0, err2);
+
+ return (num, typ, sizeTag + sizeValue, null);
+ }
+
+ ///
+ /// Reads a protobuf tag varint and returns field number, wire type, and bytes consumed.
+ /// Mirrors protoScanTag.
+ ///
+ public static (int num, int typ, int size, Exception? err) ScanTag(ReadOnlySpan b)
+ {
+ var (tagint, size, err) = ScanVarint(b);
+ if (err != null) return (0, 0, 0, err);
+
+ if ((tagint >> 3) > int.MaxValue)
+ return (0, 0, 0, ErrInvalidFieldNumber);
+
+ var num = (int)(tagint >> 3);
+ if (num < 1)
+ return (0, 0, 0, ErrInvalidFieldNumber);
+
+ var typ = (int)(tagint & 7);
+ return (num, typ, size, null);
+ }
+
+ ///
+ /// Returns the byte count consumed by a field value with the given wire type.
+ /// Mirrors protoScanFieldValue.
+ ///
+ public static (int size, Exception? err) ScanFieldValue(int typ, ReadOnlySpan b)
+ {
+ switch (typ)
+ {
+ case 0: // varint
+ {
+ var (_, size, err) = ScanVarint(b);
+ return (size, err);
+ }
+ case 5: // fixed32
+ return (4, null);
+ case 1: // fixed64
+ return (8, null);
+ case 2: // length-delimited
+ {
+ var (size, err) = ScanBytes(b);
+ return (size, err);
+ }
+ default:
+ return (0, new InvalidOperationException($"unsupported type: {typ}"));
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Varint decode
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Decodes a protobuf varint from .
+ /// Returns (value, bytes_consumed, error).
+ /// Mirrors protoScanVarint.
+ ///
+ public static (ulong v, int size, Exception? err) ScanVarint(ReadOnlySpan b)
+ {
+ if (b.Length < 1) return (0, 0, ErrInsufficient);
+ ulong v = b[0];
+ if (v < 0x80) return (v, 1, null);
+ v -= 0x80;
+
+ if (b.Length < 2) return (0, 0, ErrInsufficient);
+ ulong y = b[1];
+ v += y << 7;
+ if (y < 0x80) return (v, 2, null);
+ v -= 0x80UL << 7;
+
+ if (b.Length < 3) return (0, 0, ErrInsufficient);
+ y = b[2];
+ v += y << 14;
+ if (y < 0x80) return (v, 3, null);
+ v -= 0x80UL << 14;
+
+ if (b.Length < 4) return (0, 0, ErrInsufficient);
+ y = b[3];
+ v += y << 21;
+ if (y < 0x80) return (v, 4, null);
+ v -= 0x80UL << 21;
+
+ if (b.Length < 5) return (0, 0, ErrInsufficient);
+ y = b[4];
+ v += y << 28;
+ if (y < 0x80) return (v, 5, null);
+ v -= 0x80UL << 28;
+
+ if (b.Length < 6) return (0, 0, ErrInsufficient);
+ y = b[5];
+ v += y << 35;
+ if (y < 0x80) return (v, 6, null);
+ v -= 0x80UL << 35;
+
+ if (b.Length < 7) return (0, 0, ErrInsufficient);
+ y = b[6];
+ v += y << 42;
+ if (y < 0x80) return (v, 7, null);
+ v -= 0x80UL << 42;
+
+ if (b.Length < 8) return (0, 0, ErrInsufficient);
+ y = b[7];
+ v += y << 49;
+ if (y < 0x80) return (v, 8, null);
+ v -= 0x80UL << 49;
+
+ if (b.Length < 9) return (0, 0, ErrInsufficient);
+ y = b[8];
+ v += y << 56;
+ if (y < 0x80) return (v, 9, null);
+ v -= 0x80UL << 56;
+
+ if (b.Length < 10) return (0, 0, ErrInsufficient);
+ y = b[9];
+ v += y << 63;
+ if (y < 2) return (v, 10, null);
+
+ return (0, 0, ErrOverflow);
+ }
+
+ // -------------------------------------------------------------------------
+ // Length-delimited decode
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Returns the total byte count consumed by a length-delimited field
+ /// (length varint + content).
+ /// Mirrors protoScanBytes.
+ ///
+ public static (int size, Exception? err) ScanBytes(ReadOnlySpan b)
+ {
+ var (l, lenSize, err) = ScanVarint(b);
+ if (err != null) return (0, err);
+
+ if (l > (ulong)(b.Length - lenSize))
+ return (0, ErrInsufficient);
+
+ return (lenSize + (int)l, null);
+ }
+
+ // -------------------------------------------------------------------------
+ // Varint encode
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Encodes a as a protobuf varint.
+ /// Mirrors protoEncodeVarint.
+ ///
+ public static byte[] EncodeVarint(ulong v)
+ {
+ if (v < 1UL << 7)
+ return [(byte)v];
+
+ if (v < 1UL << 14)
+ return [(byte)((v >> 0) & 0x7F | 0x80), (byte)(v >> 7)];
+
+ if (v < 1UL << 21)
+ return [
+ (byte)((v >> 0) & 0x7F | 0x80),
+ (byte)((v >> 7) & 0x7F | 0x80),
+ (byte)(v >> 14)];
+
+ if (v < 1UL << 28)
+ return [
+ (byte)((v >> 0) & 0x7F | 0x80),
+ (byte)((v >> 7) & 0x7F | 0x80),
+ (byte)((v >> 14) & 0x7F | 0x80),
+ (byte)(v >> 21)];
+
+ if (v < 1UL << 35)
+ return [
+ (byte)((v >> 0) & 0x7F | 0x80),
+ (byte)((v >> 7) & 0x7F | 0x80),
+ (byte)((v >> 14) & 0x7F | 0x80),
+ (byte)((v >> 21) & 0x7F | 0x80),
+ (byte)(v >> 28)];
+
+ if (v < 1UL << 42)
+ return [
+ (byte)((v >> 0) & 0x7F | 0x80),
+ (byte)((v >> 7) & 0x7F | 0x80),
+ (byte)((v >> 14) & 0x7F | 0x80),
+ (byte)((v >> 21) & 0x7F | 0x80),
+ (byte)((v >> 28) & 0x7F | 0x80),
+ (byte)(v >> 35)];
+
+ if (v < 1UL << 49)
+ return [
+ (byte)((v >> 0) & 0x7F | 0x80),
+ (byte)((v >> 7) & 0x7F | 0x80),
+ (byte)((v >> 14) & 0x7F | 0x80),
+ (byte)((v >> 21) & 0x7F | 0x80),
+ (byte)((v >> 28) & 0x7F | 0x80),
+ (byte)((v >> 35) & 0x7F | 0x80),
+ (byte)(v >> 42)];
+
+ if (v < 1UL << 56)
+ return [
+ (byte)((v >> 0) & 0x7F | 0x80),
+ (byte)((v >> 7) & 0x7F | 0x80),
+ (byte)((v >> 14) & 0x7F | 0x80),
+ (byte)((v >> 21) & 0x7F | 0x80),
+ (byte)((v >> 28) & 0x7F | 0x80),
+ (byte)((v >> 35) & 0x7F | 0x80),
+ (byte)((v >> 42) & 0x7F | 0x80),
+ (byte)(v >> 49)];
+
+ if (v < 1UL << 63)
+ return [
+ (byte)((v >> 0) & 0x7F | 0x80),
+ (byte)((v >> 7) & 0x7F | 0x80),
+ (byte)((v >> 14) & 0x7F | 0x80),
+ (byte)((v >> 21) & 0x7F | 0x80),
+ (byte)((v >> 28) & 0x7F | 0x80),
+ (byte)((v >> 35) & 0x7F | 0x80),
+ (byte)((v >> 42) & 0x7F | 0x80),
+ (byte)((v >> 49) & 0x7F | 0x80),
+ (byte)(v >> 56)];
+
+ return [
+ (byte)((v >> 0) & 0x7F | 0x80),
+ (byte)((v >> 7) & 0x7F | 0x80),
+ (byte)((v >> 14) & 0x7F | 0x80),
+ (byte)((v >> 21) & 0x7F | 0x80),
+ (byte)((v >> 28) & 0x7F | 0x80),
+ (byte)((v >> 35) & 0x7F | 0x80),
+ (byte)((v >> 42) & 0x7F | 0x80),
+ (byte)((v >> 49) & 0x7F | 0x80),
+ (byte)((v >> 56) & 0x7F | 0x80),
+ 1];
+ }
+}
diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/RateCounter.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/RateCounter.cs
new file mode 100644
index 0000000..e1eec15
--- /dev/null
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/RateCounter.cs
@@ -0,0 +1,81 @@
+// Copyright 2021-2025 The NATS Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// Adapted from server/rate_counter.go in the NATS server Go source.
+
+namespace ZB.MOM.NatsNet.Server.Internal;
+
+///
+/// A sliding-window rate limiter that allows at most limit events
+/// per window.
+/// Mirrors rateCounter in server/rate_counter.go.
+///
+public sealed class RateCounter
+{
+ private readonly long _limit;
+ private long _count;
+ private ulong _blocked;
+ private DateTime _end;
+
+ // Exposed for tests (mirrors direct field access in rate_counter_test.go).
+ public TimeSpan Interval;
+
+ private readonly object _lock = new();
+
+ public RateCounter(long limit)
+ {
+ _limit = limit;
+ Interval = TimeSpan.FromSeconds(1);
+ }
+
+ ///
+ /// Returns true if the event is within the rate limit for the current window.
+ /// Mirrors rateCounter.allow.
+ ///
+ public bool Allow()
+ {
+ var now = DateTime.UtcNow;
+
+ lock (_lock)
+ {
+ if (now > _end)
+ {
+ _count = 0;
+ _end = now + Interval;
+ }
+ else
+ {
+ _count++;
+ }
+
+ var allow = _count < _limit;
+ if (!allow)
+ _blocked++;
+ return allow;
+ }
+ }
+
+ ///
+ /// Returns and resets the count of blocked events since the last call.
+ /// Mirrors rateCounter.countBlocked.
+ ///
+ public ulong CountBlocked()
+ {
+ lock (_lock)
+ {
+ var blocked = _blocked;
+ _blocked = 0;
+ return blocked;
+ }
+ }
+}
diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ServerConstants.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ServerConstants.cs
new file mode 100644
index 0000000..40e8a45
--- /dev/null
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/ServerConstants.cs
@@ -0,0 +1,228 @@
+// Copyright 2012-2025 The NATS Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// Adapted from server/const.go in the NATS server Go source.
+
+using System.Reflection;
+
+namespace ZB.MOM.NatsNet.Server;
+
+///
+/// Server-wide constants and version information.
+/// Mirrors server/const.go.
+///
+public static class ServerConstants
+{
+ // Server version — mirrors VERSION in const.go.
+ public const string Version = "2.14.0-dev";
+
+ // Protocol version — mirrors PROTO in const.go.
+ public const int Proto = 1;
+
+ // Default port for client connections — mirrors DEFAULT_PORT.
+ public const int DefaultPort = 4222;
+
+ // Sentinel port value that triggers a random port selection — mirrors RANDOM_PORT.
+ public const int RandomPort = -1;
+
+ // Default bind address — mirrors DEFAULT_HOST.
+ public const string DefaultHost = "0.0.0.0";
+
+ // Maximum allowed control line size (4 KB) — mirrors MAX_CONTROL_LINE_SIZE.
+ public const int MaxControlLineSize = 4096;
+
+ // Maximum allowed payload size (1 MB) — mirrors MAX_PAYLOAD_SIZE.
+ public const int MaxPayloadSize = 1024 * 1024;
+
+ // Payload size above which the server warns — mirrors MAX_PAYLOAD_MAX_SIZE.
+ public const int MaxPayloadMaxSize = 8 * 1024 * 1024;
+
+ // Maximum outbound pending bytes per client (64 MB) — mirrors MAX_PENDING_SIZE.
+ public const int MaxPendingSize = 64 * 1024 * 1024;
+
+ // Default maximum connections allowed (64 K) — mirrors DEFAULT_MAX_CONNECTIONS.
+ public const int DefaultMaxConnections = 64 * 1024;
+
+ // TLS handshake timeout — mirrors TLS_TIMEOUT.
+ public static readonly TimeSpan TlsTimeout = TimeSpan.FromSeconds(2);
+
+ // Fallback delay before sending INFO when using TLSHandshakeFirst
+ // — mirrors DEFAULT_TLS_HANDSHAKE_FIRST_FALLBACK_DELAY.
+ public static readonly TimeSpan DefaultTlsHandshakeFirstFallbackDelay = TimeSpan.FromMilliseconds(50);
+
+ // Auth timeout — mirrors AUTH_TIMEOUT.
+ public static readonly TimeSpan AuthTimeout = TimeSpan.FromSeconds(2);
+
+ // How often pings are sent — mirrors DEFAULT_PING_INTERVAL.
+ public static readonly TimeSpan DefaultPingInterval = TimeSpan.FromMinutes(2);
+
+ // Maximum pings outstanding before disconnect — mirrors DEFAULT_PING_MAX_OUT.
+ public const int DefaultPingMaxOut = 2;
+
+ // CR LF end-of-line — mirrors CR_LF.
+ public const string CrLf = "\r\n";
+
+ // Length of CR_LF — mirrors LEN_CR_LF.
+ public const int LenCrLf = 2;
+
+ // Write/flush deadline — mirrors DEFAULT_FLUSH_DEADLINE.
+ public static readonly TimeSpan DefaultFlushDeadline = TimeSpan.FromSeconds(10);
+
+ // Default monitoring port — mirrors DEFAULT_HTTP_PORT.
+ public const int DefaultHttpPort = 8222;
+
+ // Default monitoring base path — mirrors DEFAULT_HTTP_BASE_PATH.
+ public const string DefaultHttpBasePath = "/";
+
+ // Minimum sleep on temporary accept errors — mirrors ACCEPT_MIN_SLEEP.
+ public static readonly TimeSpan AcceptMinSleep = TimeSpan.FromMilliseconds(10);
+
+ // Maximum sleep on temporary accept errors — mirrors ACCEPT_MAX_SLEEP.
+ public static readonly TimeSpan AcceptMaxSleep = TimeSpan.FromSeconds(1);
+
+ // Route solicitation interval — mirrors DEFAULT_ROUTE_CONNECT.
+ public static readonly TimeSpan DefaultRouteConnect = TimeSpan.FromSeconds(1);
+
+ // Maximum route solicitation interval — mirrors DEFAULT_ROUTE_CONNECT_MAX.
+ public static readonly TimeSpan DefaultRouteConnectMax = TimeSpan.FromSeconds(30);
+
+ // Route reconnect delay — mirrors DEFAULT_ROUTE_RECONNECT.
+ public static readonly TimeSpan DefaultRouteReconnect = TimeSpan.FromSeconds(1);
+
+ // Route dial timeout — mirrors DEFAULT_ROUTE_DIAL.
+ public static readonly TimeSpan DefaultRouteDial = TimeSpan.FromSeconds(1);
+
+ // Default route pool size — mirrors DEFAULT_ROUTE_POOL_SIZE.
+ public const int DefaultRoutePoolSize = 3;
+
+ // LeafNode reconnect interval — mirrors DEFAULT_LEAF_NODE_RECONNECT.
+ public static readonly TimeSpan DefaultLeafNodeReconnect = TimeSpan.FromSeconds(1);
+
+ // TLS timeout for leaf nodes — mirrors DEFAULT_LEAF_TLS_TIMEOUT.
+ public static readonly TimeSpan DefaultLeafTlsTimeout = TimeSpan.FromSeconds(2);
+
+ // Proto snippet size for parse error display — mirrors PROTO_SNIPPET_SIZE.
+ public const int ProtoSnippetSize = 32;
+
+ // Max control line snippet size for error display — mirrors MAX_CONTROL_LINE_SNIPPET_SIZE.
+ public const int MaxControlLineSnippetSize = 128;
+
+ // Maximum MSG proto argument count — mirrors MAX_MSG_ARGS.
+ public const int MaxMsgArgs = 4;
+
+ // Maximum RMSG proto argument count — mirrors MAX_RMSG_ARGS.
+ public const int MaxRMsgArgs = 6;
+
+ // Maximum HMSG proto argument count — mirrors MAX_HMSG_ARGS.
+ public const int MaxHMsgArgs = 7;
+
+ // Maximum PUB proto argument count — mirrors MAX_PUB_ARGS.
+ public const int MaxPubArgs = 3;
+
+ // Maximum HPUB proto argument count — mirrors MAX_HPUB_ARGS.
+ public const int MaxHPubArgs = 4;
+
+ // Maximum RS+/LS+ proto argument count — mirrors MAX_RSUB_ARGS.
+ public const int MaxRSubArgs = 6;
+
+ // Maximum closed connections retained — mirrors DEFAULT_MAX_CLOSED_CLIENTS.
+ public const int DefaultMaxClosedClients = 10000;
+
+ // Lame duck spread duration — mirrors DEFAULT_LAME_DUCK_DURATION.
+ public static readonly TimeSpan DefaultLameDuckDuration = TimeSpan.FromMinutes(2);
+
+ // Lame duck grace period — mirrors DEFAULT_LAME_DUCK_GRACE_PERIOD.
+ public static readonly TimeSpan DefaultLameDuckGracePeriod = TimeSpan.FromSeconds(10);
+
+ // Leaf node INFO wait — mirrors DEFAULT_LEAFNODE_INFO_WAIT.
+ public static readonly TimeSpan DefaultLeafNodeInfoWait = TimeSpan.FromSeconds(1);
+
+ // Default leaf node port — mirrors DEFAULT_LEAFNODE_PORT.
+ public const int DefaultLeafNodePort = 7422;
+
+ // Connect error report threshold — mirrors DEFAULT_CONNECT_ERROR_REPORTS.
+ public const int DefaultConnectErrorReports = 3600;
+
+ // Reconnect error report threshold — mirrors DEFAULT_RECONNECT_ERROR_REPORTS.
+ public const int DefaultReconnectErrorReports = 1;
+
+ // RTT measurement interval — mirrors DEFAULT_RTT_MEASUREMENT_INTERVAL.
+ public static readonly TimeSpan DefaultRttMeasurementInterval = TimeSpan.FromHours(1);
+
+ // Default allowed response max messages — mirrors DEFAULT_ALLOW_RESPONSE_MAX_MSGS.
+ public const int DefaultAllowResponseMaxMsgs = 1;
+
+ // Default allowed response expiration — mirrors DEFAULT_ALLOW_RESPONSE_EXPIRATION.
+ public static readonly TimeSpan DefaultAllowResponseExpiration = TimeSpan.FromMinutes(2);
+
+ // Default service export response threshold — mirrors DEFAULT_SERVICE_EXPORT_RESPONSE_THRESHOLD.
+ public static readonly TimeSpan DefaultServiceExportResponseThreshold = TimeSpan.FromMinutes(2);
+
+ // Default service latency sampling rate — mirrors DEFAULT_SERVICE_LATENCY_SAMPLING.
+ public const int DefaultServiceLatencySampling = 100;
+
+ // Default system account name — mirrors DEFAULT_SYSTEM_ACCOUNT.
+ public const string DefaultSystemAccount = "$SYS";
+
+ // Default global account name — mirrors DEFAULT_GLOBAL_ACCOUNT.
+ public const string DefaultGlobalAccount = "$G";
+
+ // Default account fetch timeout — mirrors DEFAULT_ACCOUNT_FETCH_TIMEOUT.
+ public static readonly TimeSpan DefaultAccountFetchTimeout = TimeSpan.FromMilliseconds(1900);
+
+ // VCS commit hash embedded at build time, shortened to 7 chars — mirrors gitCommit.
+ // Populated from AssemblyInformationalVersion metadata if available.
+ public static readonly string GitCommit;
+
+ static ServerConstants()
+ {
+ // Mirror const.go init(): extract VCS revision from build info.
+ // In .NET we read the AssemblyInformationalVersion attribute which
+ // is typically set to the semantic version + commit hash by dotnet publish.
+ var infoVersion = typeof(ServerConstants).Assembly
+ .GetCustomAttribute()
+ ?.InformationalVersion;
+
+ if (infoVersion != null)
+ {
+ // Convention: "1.2.3+abcdefg" or "1.2.3-dev+abcdefg"
+ var plusIdx = infoVersion.IndexOf('+');
+ if (plusIdx >= 0)
+ {
+ var rev = infoVersion[(plusIdx + 1)..];
+ GitCommit = FormatRevision(rev);
+ return;
+ }
+ }
+
+ GitCommit = string.Empty;
+ }
+
+ ///
+ /// Truncates a VCS revision string to 7 characters for display.
+ /// Mirrors formatRevision in const.go.
+ ///
+ public static string FormatRevision(string revision) =>
+ revision.Length >= 7 ? revision[..7] : revision;
+}
+
+///
+/// Server control commands — mirrors the Command type in const.go.
+///
+public enum ServerCommand
+{
+ Stop,
+ Quit,
+ Reopen,
+ Reload,
+}
diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ServerErrors.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ServerErrors.cs
new file mode 100644
index 0000000..2558140
--- /dev/null
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/ServerErrors.cs
@@ -0,0 +1,446 @@
+// Copyright 2012-2025 The NATS Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// Adapted from server/errors.go in the NATS server Go source.
+// Note: errorsUnwrap() and ErrorIs() are Go 1.12 compat shims — .NET has
+// Exception.InnerException and errors.Is() equivalents built in;
+// both are represented here for completeness but map to built-in .NET patterns.
+
+namespace ZB.MOM.NatsNet.Server;
+
+// -------------------------------------------------------------------------
+// Sentinel error values (mirrors package-level var block in errors.go)
+// -------------------------------------------------------------------------
+
+///
+/// Standard NATS server error sentinels.
+/// Mirrors the package-level var error block in server/errors.go.
+///
+public static class ServerErrors
+{
+ public static readonly Exception ErrConnectionClosed =
+ new InvalidOperationException("connection closed");
+
+ public static readonly Exception ErrAuthentication =
+ new InvalidOperationException("authentication error");
+
+ public static readonly Exception ErrAuthTimeout =
+ new InvalidOperationException("authentication timeout");
+
+ public static readonly Exception ErrAuthExpired =
+ new InvalidOperationException("authentication expired");
+
+ public static readonly Exception ErrAuthProxyNotTrusted =
+ new InvalidOperationException("proxy is not trusted");
+
+ public static readonly Exception ErrAuthProxyRequired =
+ new InvalidOperationException("proxy connection required");
+
+ public static readonly Exception ErrMaxPayload =
+ new InvalidOperationException("maximum payload exceeded");
+
+ public static readonly Exception ErrMaxControlLine =
+ new InvalidOperationException("maximum control line exceeded");
+
+ public static readonly Exception ErrReservedPublishSubject =
+ new InvalidOperationException("reserved internal subject");
+
+ public static readonly Exception ErrBadPublishSubject =
+ new InvalidOperationException("invalid publish subject");
+
+ public static readonly Exception ErrBadSubject =
+ new InvalidOperationException("invalid subject");
+
+ public static readonly Exception ErrBadQualifier =
+ new InvalidOperationException("bad qualifier");
+
+ public static readonly Exception ErrBadClientProtocol =
+ new InvalidOperationException("invalid client protocol");
+
+ public static readonly Exception ErrTooManyConnections =
+ new InvalidOperationException("maximum connections exceeded");
+
+ public static readonly Exception ErrTooManyAccountConnections =
+ new InvalidOperationException("maximum account active connections exceeded");
+
+ public static readonly Exception ErrLeafNodeLoop =
+ new InvalidOperationException("leafnode loop detected");
+
+ public static readonly Exception ErrTooManySubs =
+ new InvalidOperationException("maximum subscriptions exceeded");
+
+ public static readonly Exception ErrTooManySubTokens =
+ new InvalidOperationException("subject has exceeded number of tokens limit");
+
+ public static readonly Exception ErrClientConnectedToRoutePort =
+ new InvalidOperationException("attempted to connect to route port");
+
+ public static readonly Exception ErrClientConnectedToLeafNodePort =
+ new InvalidOperationException("attempted to connect to leaf node port");
+
+ public static readonly Exception ErrLeafNodeHasSameClusterName =
+ new InvalidOperationException("remote leafnode has same cluster name");
+
+ public static readonly Exception ErrLeafNodeDisabled =
+ new InvalidOperationException("leafnodes disabled");
+
+ public static readonly Exception ErrConnectedToWrongPort =
+ new InvalidOperationException("attempted to connect to wrong port");
+
+ public static readonly Exception ErrAccountExists =
+ new InvalidOperationException("account exists");
+
+ public static readonly Exception ErrBadAccount =
+ new InvalidOperationException("bad account");
+
+ public static readonly Exception ErrReservedAccount =
+ new InvalidOperationException("reserved account");
+
+ public static readonly Exception ErrMissingAccount =
+ new InvalidOperationException("account missing");
+
+ public static readonly Exception ErrMissingService =
+ new InvalidOperationException("service missing");
+
+ public static readonly Exception ErrBadServiceType =
+ new InvalidOperationException("bad service response type");
+
+ public static readonly Exception ErrBadSampling =
+ new InvalidOperationException("bad sampling percentage, should be 1-100");
+
+ public static readonly Exception ErrAccountValidation =
+ new InvalidOperationException("account validation failed");
+
+ public static readonly Exception ErrAccountExpired =
+ new InvalidOperationException("account expired");
+
+ public static readonly Exception ErrNoAccountResolver =
+ new InvalidOperationException("account resolver missing");
+
+ public static readonly Exception ErrAccountResolverUpdateTooSoon =
+ new InvalidOperationException("account resolver update too soon");
+
+ public static readonly Exception ErrAccountResolverSameClaims =
+ new InvalidOperationException("account resolver no new claims");
+
+ public static readonly Exception ErrStreamImportAuthorization =
+ new InvalidOperationException("stream import not authorized");
+
+ public static readonly Exception ErrStreamImportBadPrefix =
+ new InvalidOperationException("stream import prefix can not contain wildcard tokens");
+
+ public static readonly Exception ErrStreamImportDuplicate =
+ new InvalidOperationException("stream import already exists");
+
+ public static readonly Exception ErrServiceImportAuthorization =
+ new InvalidOperationException("service import not authorized");
+
+ public static readonly Exception ErrImportFormsCycle =
+ new InvalidOperationException("import forms a cycle");
+
+ public static readonly Exception ErrCycleSearchDepth =
+ new InvalidOperationException("search cycle depth exhausted");
+
+ public static readonly Exception ErrClientOrRouteConnectedToGatewayPort =
+ new InvalidOperationException("attempted to connect to gateway port");
+
+ public static readonly Exception ErrWrongGateway =
+ new InvalidOperationException("wrong gateway");
+
+ public static readonly Exception ErrGatewayNameHasSpaces =
+ new InvalidOperationException("gateway name cannot contain spaces");
+
+ public static readonly Exception ErrNoSysAccount =
+ new InvalidOperationException("system account not setup");
+
+ public static readonly Exception ErrRevocation =
+ new InvalidOperationException("credentials have been revoked");
+
+ public static readonly Exception ErrServerNotRunning =
+ new InvalidOperationException("server is not running");
+
+ public static readonly Exception ErrServerNameHasSpaces =
+ new InvalidOperationException("server name cannot contain spaces");
+
+ public static readonly Exception ErrBadMsgHeader =
+ new InvalidOperationException("bad message header detected");
+
+ public static readonly Exception ErrMsgHeadersNotSupported =
+ new InvalidOperationException("message headers not supported");
+
+ public static readonly Exception ErrNoRespondersRequiresHeaders =
+ new InvalidOperationException("no responders requires headers support");
+
+ public static readonly Exception ErrClusterNameConfigConflict =
+ new InvalidOperationException("cluster name conflicts between cluster and gateway definitions");
+
+ public static readonly Exception ErrClusterNameRemoteConflict =
+ new InvalidOperationException("cluster name from remote server conflicts");
+
+ public static readonly Exception ErrClusterNameHasSpaces =
+ new InvalidOperationException("cluster name cannot contain spaces");
+
+ public static readonly Exception ErrMalformedSubject =
+ new InvalidOperationException("malformed subject");
+
+ public static readonly Exception ErrSubscribePermissionViolation =
+ new InvalidOperationException("subscribe permission violation");
+
+ public static readonly Exception ErrNoTransforms =
+ new InvalidOperationException("no matching transforms available");
+
+ public static readonly Exception ErrCertNotPinned =
+ new InvalidOperationException("certificate not pinned");
+
+ public static readonly Exception ErrDuplicateServerName =
+ new InvalidOperationException("duplicate server name");
+
+ public static readonly Exception ErrMinimumVersionRequired =
+ new InvalidOperationException("minimum version required");
+
+ // Mapping destination errors — the Go source wraps ErrInvalidMappingDestination.
+ // In .NET we use a common base message and chain inner exceptions where needed.
+ public static readonly Exception ErrInvalidMappingDestination =
+ new InvalidOperationException("invalid mapping destination");
+
+ public static readonly Exception ErrInvalidMappingDestinationSubject =
+ new InvalidOperationException("invalid mapping destination: invalid transform");
+
+ public static readonly Exception ErrMappingDestinationNotUsingAllWildcards =
+ new InvalidOperationException("invalid mapping destination: not using all of the token wildcard(s)");
+
+ public static readonly Exception ErrUnknownMappingDestinationFunction =
+ new InvalidOperationException("invalid mapping destination: unknown function");
+
+ public static readonly Exception ErrMappingDestinationIndexOutOfRange =
+ new InvalidOperationException("invalid mapping destination: wildcard index out of range");
+
+ public static readonly Exception ErrMappingDestinationNotEnoughArgs =
+ new InvalidOperationException("invalid mapping destination: not enough arguments passed to the function");
+
+ public static readonly Exception ErrMappingDestinationInvalidArg =
+ new InvalidOperationException("invalid mapping destination: function argument is invalid or in the wrong format");
+
+ public static readonly Exception ErrMappingDestinationTooManyArgs =
+ new InvalidOperationException("invalid mapping destination: too many arguments passed to the function");
+
+ public static readonly Exception ErrMappingDestinationNotSupportedForImport =
+ new InvalidOperationException("invalid mapping destination: the only mapping function allowed for import transforms is {{Wildcard()}}");
+}
+
+// -------------------------------------------------------------------------
+// mappingDestinationErr (mirrors server/errors.go)
+// -------------------------------------------------------------------------
+
+///
+/// An error type for subject mapping destination validation failures.
+/// Mirrors mappingDestinationErr in server/errors.go.
+///
+public sealed class MappingDestinationException : Exception
+{
+ private readonly string _token;
+
+ public MappingDestinationException(string token, Exception inner)
+ : base($"{inner.Message} in {token}", inner)
+ {
+ _token = token;
+ }
+
+ // Is() in Go is implemented via IsInvalidMappingDestination below.
+
+ ///
+ /// Returns true when compared against .
+ /// Mirrors mappingDestinationErr.Is.
+ ///
+ public bool Is(Exception target) =>
+ ReferenceEquals(target, ServerErrors.ErrInvalidMappingDestination);
+}
+
+// -------------------------------------------------------------------------
+// configErr / unknownConfigFieldErr / configWarningErr (mirrors server/errors.go)
+// -------------------------------------------------------------------------
+// Note: these types depend on a config-file token interface defined in the
+// configuration parser (ported in session 03). Forward-declared here with the
+// minimal interface needed for error formatting.
+
+///
+/// Represents a source location within a configuration file.
+/// Mirrors the token interface used by configErr in server/errors.go.
+/// Full implementation is provided by the configuration parser (session 03).
+///
+public interface IConfigToken
+{
+ string SourceFile();
+ int Line();
+ int Position();
+}
+
+///
+/// A configuration parsing error with source location.
+/// Mirrors configErr in server/errors.go.
+///
+public class ConfigException : Exception
+{
+ private readonly IConfigToken? _token;
+
+ public ConfigException(IConfigToken? token, string reason)
+ : base(reason)
+ {
+ _token = token;
+ }
+
+ /// Returns "file:line:col" source location, or empty string if no token.
+ public new string Source() =>
+ _token != null
+ ? $"{_token.SourceFile()}:{_token.Line()}:{_token.Position()}"
+ : string.Empty;
+
+ public override string Message =>
+ _token != null ? $"{Source()}: {base.Message}" : base.Message;
+}
+
+///
+/// A configuration error for an unknown field (pedantic mode).
+/// Mirrors unknownConfigFieldErr in server/errors.go.
+///
+public sealed class UnknownConfigFieldException : ConfigException
+{
+ private readonly string _field;
+
+ public UnknownConfigFieldException(IConfigToken token, string field)
+ : base(token, $"unknown field \"{field}\"")
+ {
+ _field = field;
+ }
+}
+
+///
+/// A configuration warning for invalid field usage (pedantic mode).
+/// Mirrors configWarningErr in server/errors.go.
+///
+public sealed class ConfigWarningException : ConfigException
+{
+ private readonly string _field;
+
+ public ConfigWarningException(IConfigToken token, string field, string reason)
+ : base(token, $"invalid use of field \"{field}\": {reason}")
+ {
+ _field = field;
+ }
+}
+
+///
+/// Aggregates configuration warnings and hard errors from parsing.
+/// Mirrors processConfigErr in server/errors.go.
+///
+public sealed class ProcessConfigException : Exception
+{
+ public IReadOnlyList Warnings { get; }
+ public IReadOnlyList Errors { get; }
+
+ public ProcessConfigException(IReadOnlyList errors, IReadOnlyList warnings)
+ : base(BuildMessage(errors, warnings))
+ {
+ Errors = errors;
+ Warnings = warnings;
+ }
+
+ private static string BuildMessage(IReadOnlyList errors, IReadOnlyList warnings)
+ {
+ var sb = new System.Text.StringBuilder();
+ foreach (var w in warnings) sb.AppendLine(w.Message);
+ foreach (var e in errors) sb.AppendLine(e.Message);
+ return sb.ToString();
+ }
+}
+
+// -------------------------------------------------------------------------
+// errCtx — error with attached tracing context (mirrors server/errors.go)
+// -------------------------------------------------------------------------
+
+///
+/// Wraps an exception and attaches additional tracing context.
+/// Context is not included in but is
+/// accessible via and .
+/// Mirrors errCtx in server/errors.go.
+///
+public sealed class ErrorCtx : Exception
+{
+ public string Ctx { get; }
+
+ public ErrorCtx(Exception inner, string ctx)
+ : base(inner.Message, inner)
+ {
+ Ctx = ctx;
+ }
+
+ ///
+ /// Returns the context string attached to this error.
+ /// Mirrors errCtx.Context().
+ ///
+ public string Context() => Ctx;
+}
+
+///
+/// Factory and utilities for .
+/// Mirrors NewErrorCtx, UnpackIfErrorCtx, and ErrorIs in server/errors.go.
+///
+public static class ErrorContextHelper
+{
+ ///
+ /// Creates an wrapping with formatted context.
+ /// Mirrors NewErrorCtx.
+ ///
+ public static Exception NewErrorCtx(Exception err, string format, params object[] args) =>
+ new ErrorCtx(err, string.Format(format, args));
+
+ ///
+ /// If is an , returns
+ /// "original.Message: ctx" (recursively). Otherwise returns err.Message.
+ /// Mirrors UnpackIfErrorCtx.
+ ///
+ public static string UnpackIfErrorCtx(Exception err)
+ {
+ if (err is ErrorCtx ectx)
+ {
+ if (ectx.InnerException is ErrorCtx)
+ return $"{UnpackIfErrorCtx(ectx.InnerException!)}: {ectx.Ctx}";
+ return $"{ectx.InnerException!.Message}: {ectx.Ctx}";
+ }
+ return err.Message;
+ }
+
+ ///
+ /// Walks the inner exception chain checking reference equality and
+ /// overrides.
+ /// In .NET, prefer errors.Is-equivalent patterns; this mirrors
+ /// the Go ErrorIs shim exactly.
+ /// Mirrors ErrorIs.
+ ///
+ public static bool ErrorIs(Exception? err, Exception? target)
+ {
+ if (err == null || target == null)
+ return ReferenceEquals(err, target);
+
+ var current = err;
+ while (current != null)
+ {
+ if (ReferenceEquals(current, target))
+ return true;
+ if (current is MappingDestinationException mde && mde.Is(target))
+ return true;
+ current = current.InnerException;
+ }
+ return false;
+ }
+}
diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Foundation/ServerErrorsTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Foundation/ServerErrorsTests.cs
new file mode 100644
index 0000000..19153ab
--- /dev/null
+++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Foundation/ServerErrorsTests.cs
@@ -0,0 +1,87 @@
+// Copyright 2020-2025 The NATS Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Shouldly;
+using ZB.MOM.NatsNet.Server;
+
+namespace ZB.MOM.NatsNet.Server.Tests.Foundation;
+
+///
+/// Tests for and .
+/// Mirrors server/errors_test.go: TestErrCtx (ID 297) and TestErrCtxWrapped (ID 298).
+///
+public sealed class ServerErrorsTests
+{
+ [Fact]
+ public void ErrCtx_ShouldPreserveOriginalMessageAndAddContext()
+ {
+ // Mirror: TestErrCtx
+ var ctx = "Extra context information";
+ var e = ErrorContextHelper.NewErrorCtx(ServerErrors.ErrWrongGateway, "{0}", ctx);
+
+ // Message should match the underlying error.
+ e.Message.ShouldBe(ServerErrors.ErrWrongGateway.Message);
+
+ // Must not be reference-equal to the sentinel.
+ e.ShouldNotBeSameAs(ServerErrors.ErrWrongGateway);
+
+ // ErrorIs should find the sentinel in the chain.
+ ErrorContextHelper.ErrorIs(e, ServerErrors.ErrWrongGateway).ShouldBeTrue();
+
+ // UnpackIfErrorCtx on a non-ctx error returns Message unchanged.
+ ErrorContextHelper.UnpackIfErrorCtx(ServerErrors.ErrWrongGateway)
+ .ShouldBe(ServerErrors.ErrWrongGateway.Message);
+
+ // UnpackIfErrorCtx should start with the original error message.
+ var trace = ErrorContextHelper.UnpackIfErrorCtx(e);
+ trace.ShouldStartWith(ServerErrors.ErrWrongGateway.Message);
+
+ // And end with the context string.
+ trace.ShouldEndWith(ctx);
+ }
+
+ [Fact]
+ public void ErrCtxWrapped_ShouldContainAllContextLayers()
+ {
+ // Mirror: TestErrCtxWrapped
+ var ctxO = "Original Ctx";
+ var eO = ErrorContextHelper.NewErrorCtx(ServerErrors.ErrWrongGateway, "{0}", ctxO);
+
+ var ctx = "Extra context information";
+ var e = ErrorContextHelper.NewErrorCtx(eO, "{0}", ctx);
+
+ // Message should still match the underlying error.
+ e.Message.ShouldBe(ServerErrors.ErrWrongGateway.Message);
+
+ // Must not be reference-equal to the sentinel.
+ e.ShouldNotBeSameAs(ServerErrors.ErrWrongGateway);
+
+ // ErrorIs should walk the chain.
+ ErrorContextHelper.ErrorIs(e, ServerErrors.ErrWrongGateway).ShouldBeTrue();
+
+ // UnpackIfErrorCtx on a non-ctx error returns Message unchanged.
+ ErrorContextHelper.UnpackIfErrorCtx(ServerErrors.ErrWrongGateway)
+ .ShouldBe(ServerErrors.ErrWrongGateway.Message);
+
+ var trace = ErrorContextHelper.UnpackIfErrorCtx(e);
+
+ // Must start with the original error.
+ trace.ShouldStartWith(ServerErrors.ErrWrongGateway.Message);
+
+ // Must end with the outermost context.
+ trace.ShouldEndWith(ctx);
+
+ // Must also contain the inner context.
+ trace.ShouldContain(ctxO);
+ }
+}
diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ClosedRingBufferTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ClosedRingBufferTests.cs
new file mode 100644
index 0000000..0e30c32
--- /dev/null
+++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ClosedRingBufferTests.cs
@@ -0,0 +1,79 @@
+// Copyright 2018-2025 The NATS Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Shouldly;
+using ZB.MOM.NatsNet.Server.Internal;
+
+namespace ZB.MOM.NatsNet.Server.Tests.Internal;
+
+///
+/// Tests for .
+/// Mirrors server/ring_test.go: TestRBAppendAndLenAndTotal (ID 2794)
+/// and TestRBclosedClients (ID 2795).
+///
+public sealed class ClosedRingBufferTests
+{
+ [Fact]
+ public void AppendAndLenAndTotal_ShouldTrackCorrectly()
+ {
+ // Mirror: TestRBAppendAndLenAndTotal
+ var rb = new ClosedRingBuffer(10);
+
+ for (var i = 0; i < 5; i++)
+ rb.Append(new ClosedClient());
+
+ rb.Len().ShouldBe(5);
+ rb.TotalConns().ShouldBe(5UL);
+
+ for (var i = 0; i < 25; i++)
+ rb.Append(new ClosedClient());
+
+ rb.Len().ShouldBe(10);
+ rb.TotalConns().ShouldBe(30UL);
+ }
+
+ [Fact]
+ public void ClosedClients_ShouldReturnChronologicalOrder()
+ {
+ // Mirror: TestRBclosedClients
+ var rb = new ClosedRingBuffer(10);
+
+ // Build master list with identifiable user strings.
+ const int max = 100;
+ var master = new ClosedClient[max];
+ for (var i = 1; i <= max; i++)
+ master[i - 1] = new ClosedClient { User = i.ToString() };
+
+ var ui = 0;
+
+ void AddConn()
+ {
+ ui++;
+ rb.Append(new ClosedClient { User = ui.ToString() });
+ }
+
+ for (var i = 0; i < max; i++)
+ {
+ AddConn();
+
+ var ccs = rb.ClosedClients();
+ var start = (int)rb.TotalConns() - ccs.Length;
+ var ms = master[start..(start + ccs.Length)];
+
+ // Verify order matches master using User strings.
+ ccs.Length.ShouldBe(ms.Length);
+ for (var j = 0; j < ccs.Length; j++)
+ ccs[j]!.User.ShouldBe(ms[j].User, $"iteration {i}, slot {j}");
+ }
+ }
+}
diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/RateCounterTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/RateCounterTests.cs
new file mode 100644
index 0000000..fbb97b3
--- /dev/null
+++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/RateCounterTests.cs
@@ -0,0 +1,47 @@
+// Copyright 2021-2025 The NATS Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Shouldly;
+using ZB.MOM.NatsNet.Server.Internal;
+
+namespace ZB.MOM.NatsNet.Server.Tests.Internal;
+
+///
+/// Tests for .
+/// Mirrors server/rate_counter_test.go: TestRateCounter (ID 2720).
+///
+public sealed class RateCounterTests
+{
+ [Fact]
+ public async Task RateCounter_ShouldAllowUpToLimitThenBlockAndReset()
+ {
+ // Mirror: TestRateCounter
+ var counter = new RateCounter(10) { Interval = TimeSpan.FromMilliseconds(100) };
+
+ // First 10 calls should be allowed (counts 0–9 < limit 10).
+ for (var i = 0; i < 10; i++)
+ counter.Allow().ShouldBeTrue($"should allow on iteration {i}");
+
+ // Next 5 should be blocked.
+ for (var i = 0; i < 5; i++)
+ counter.Allow().ShouldBeFalse($"should not allow on iteration {i}");
+
+ // countBlocked returns and resets the blocked count.
+ counter.CountBlocked().ShouldBe(5UL);
+ counter.CountBlocked().ShouldBe(0UL);
+
+ // After the window expires, should allow again.
+ await Task.Delay(150);
+ counter.Allow().ShouldBeTrue("should allow after window expired");
+ }
+}
diff --git a/porting.db b/porting.db
index 5e26269..b9a4b1c 100644
Binary files a/porting.db and b/porting.db differ
diff --git a/reports/current.md b/reports/current.md
index e4f7f89..7dc7ceb 100644
--- a/reports/current.md
+++ b/reports/current.md
@@ -1,28 +1,29 @@
# NATS .NET Porting Status Report
-Generated: 2026-02-26 13:07:55 UTC
+Generated: 2026-02-26 14:15:20 UTC
## Modules (12 total)
| Status | Count |
|--------|-------|
-| complete | 1 |
-| not_started | 11 |
+| complete | 11 |
+| not_started | 1 |
## Features (3673 total)
| Status | Count |
|--------|-------|
-| complete | 36 |
-| n_a | 41 |
-| not_started | 3596 |
+| complete | 265 |
+| n_a | 74 |
+| not_started | 3334 |
## Unit Tests (3257 total)
| Status | Count |
|--------|-------|
-| complete | 16 |
-| not_started | 3241 |
+| complete | 119 |
+| n_a | 12 |
+| not_started | 3126 |
## Library Mappings (36 total)
@@ -33,4 +34,4 @@ Generated: 2026-02-26 13:07:55 UTC
## Overall Progress
-**94/6942 items complete (1.4%)**
+**481/6942 items complete (6.9%)**
diff --git a/reports/report_66628bc.md b/reports/report_66628bc.md
new file mode 100644
index 0000000..7dc7ceb
--- /dev/null
+++ b/reports/report_66628bc.md
@@ -0,0 +1,37 @@
+# NATS .NET Porting Status Report
+
+Generated: 2026-02-26 14:15:20 UTC
+
+## Modules (12 total)
+
+| Status | Count |
+|--------|-------|
+| complete | 11 |
+| not_started | 1 |
+
+## Features (3673 total)
+
+| Status | Count |
+|--------|-------|
+| complete | 265 |
+| n_a | 74 |
+| not_started | 3334 |
+
+## Unit Tests (3257 total)
+
+| Status | Count |
+|--------|-------|
+| complete | 119 |
+| n_a | 12 |
+| not_started | 3126 |
+
+## Library Mappings (36 total)
+
+| Status | Count |
+|--------|-------|
+| mapped | 36 |
+
+
+## Overall Progress
+
+**481/6942 items complete (6.9%)**