diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/NatsLogger.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/NatsLogger.cs
new file mode 100644
index 0000000..b547b2c
--- /dev/null
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/NatsLogger.cs
@@ -0,0 +1,187 @@
+// 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/log.go in the NATS server Go source.
+
+using System.Collections.Concurrent;
+using Microsoft.Extensions.Logging;
+
+namespace ZB.MOM.NatsNet.Server.Internal;
+
+///
+/// NATS server Logger interface.
+/// Mirrors the Go Logger interface in log.go.
+/// In .NET we bridge to from Microsoft.Extensions.Logging.
+///
+public interface INatsLogger
+{
+ void Noticef(string format, params object[] args);
+ void Warnf(string format, params object[] args);
+ void Fatalf(string format, params object[] args);
+ void Errorf(string format, params object[] args);
+ void Debugf(string format, params object[] args);
+ void Tracef(string format, params object[] args);
+}
+
+///
+/// Server logging state. Encapsulates the logger, debug/trace flags, and rate-limiting.
+/// Mirrors the logging fields of Go's Server struct (logging struct + rateLimitLogging sync.Map).
+///
+public sealed class ServerLogging
+{
+ private readonly object _lock = new();
+ private INatsLogger? _logger;
+ private int _debug;
+ private int _trace;
+ private int _traceSysAcc;
+ private readonly ConcurrentDictionary _rateLimitMap = new();
+
+ /// Gets the current logger (thread-safe).
+ public INatsLogger? GetLogger()
+ {
+ lock (_lock) return _logger;
+ }
+
+ ///
+ /// Sets the logger with debug/trace flags.
+ /// Mirrors Server.SetLoggerV2.
+ ///
+ public void SetLoggerV2(INatsLogger? logger, bool debugFlag, bool traceFlag, bool sysTrace)
+ {
+ Interlocked.Exchange(ref _debug, debugFlag ? 1 : 0);
+ Interlocked.Exchange(ref _trace, traceFlag ? 1 : 0);
+ Interlocked.Exchange(ref _traceSysAcc, sysTrace ? 1 : 0);
+
+ lock (_lock)
+ {
+ if (_logger is IDisposable disposable)
+ disposable.Dispose();
+ _logger = logger;
+ }
+ }
+
+ ///
+ /// Sets the logger. Mirrors Server.SetLogger.
+ ///
+ public void SetLogger(INatsLogger? logger, bool debugFlag, bool traceFlag) =>
+ SetLoggerV2(logger, debugFlag, traceFlag, false);
+
+ public bool IsDebug => Interlocked.CompareExchange(ref _debug, 0, 0) != 0;
+ public bool IsTrace => Interlocked.CompareExchange(ref _trace, 0, 0) != 0;
+ public bool IsTraceSysAcc => Interlocked.CompareExchange(ref _traceSysAcc, 0, 0) != 0;
+
+ /// Executes a log call under the read lock.
+ public void ExecuteLogCall(Action action)
+ {
+ INatsLogger? logger;
+ lock (_lock) logger = _logger;
+ if (logger == null) return;
+ action(logger);
+ }
+
+ // ---- Convenience methods ----
+
+ public void Noticef(string format, params object[] args) =>
+ ExecuteLogCall(l => l.Noticef(format, args));
+
+ public void Errorf(string format, params object[] args) =>
+ ExecuteLogCall(l => l.Errorf(format, args));
+
+ public void Errors(object scope, Exception e) =>
+ ExecuteLogCall(l => l.Errorf("{0} - {1}", scope, e.Message));
+
+ public void Errorc(string ctx, Exception e) =>
+ ExecuteLogCall(l => l.Errorf("{0}: {1}", ctx, e.Message));
+
+ public void Errorsc(object scope, string ctx, Exception e) =>
+ ExecuteLogCall(l => l.Errorf("{0} - {1}: {2}", scope, ctx, e.Message));
+
+ public void Warnf(string format, params object[] args) =>
+ ExecuteLogCall(l => l.Warnf(format, args));
+
+ public void Fatalf(string format, params object[] args) =>
+ ExecuteLogCall(l => l.Fatalf(format, args));
+
+ public void Debugf(string format, params object[] args)
+ {
+ if (!IsDebug) return;
+ ExecuteLogCall(l => l.Debugf(format, args));
+ }
+
+ public void Tracef(string format, params object[] args)
+ {
+ if (!IsTrace) return;
+ ExecuteLogCall(l => l.Tracef(format, args));
+ }
+
+ ///
+ /// Rate-limited warning log. Only the first occurrence of each formatted statement is logged.
+ /// Mirrors Server.RateLimitWarnf.
+ ///
+ public void RateLimitWarnf(string format, params object[] args)
+ {
+ var statement = string.Format(format, args);
+ if (!_rateLimitMap.TryAdd(statement, DateTime.UtcNow)) return;
+ Warnf("{0}", statement);
+ }
+
+ ///
+ /// Rate-limited debug log. Only the first occurrence of each formatted statement is logged.
+ /// Mirrors Server.RateLimitDebugf.
+ ///
+ public void RateLimitDebugf(string format, params object[] args)
+ {
+ var statement = string.Format(format, args);
+ if (!_rateLimitMap.TryAdd(statement, DateTime.UtcNow)) return;
+ Debugf("{0}", statement);
+ }
+
+ ///
+ /// Rate-limited format warning. Only the first occurrence of each format string is logged.
+ /// Mirrors Server.rateLimitFormatWarnf.
+ ///
+ internal void RateLimitFormatWarnf(string format, params object[] args)
+ {
+ if (!_rateLimitMap.TryAdd(format, DateTime.UtcNow)) return;
+ var statement = string.Format(format, args);
+ Warnf("{0}", statement);
+ }
+}
+
+///
+/// Adapter that bridges to .
+///
+public sealed class MicrosoftLoggerAdapter : INatsLogger
+{
+ private readonly ILogger _logger;
+
+ public MicrosoftLoggerAdapter(ILogger logger) => _logger = logger;
+
+ public void Noticef(string format, params object[] args) =>
+ _logger.LogInformation(format, args);
+
+ public void Warnf(string format, params object[] args) =>
+ _logger.LogWarning(format, args);
+
+ public void Fatalf(string format, params object[] args) =>
+ _logger.LogCritical(format, args);
+
+ public void Errorf(string format, params object[] args) =>
+ _logger.LogError(format, args);
+
+ public void Debugf(string format, params object[] args) =>
+ _logger.LogDebug(format, args);
+
+ public void Tracef(string format, params object[] args) =>
+ _logger.LogTrace(format, args);
+}
diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/SignalHandler.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/SignalHandler.cs
new file mode 100644
index 0000000..ef55123
--- /dev/null
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/SignalHandler.cs
@@ -0,0 +1,145 @@
+// 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/signal.go and server/service.go in the NATS server Go source.
+
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace ZB.MOM.NatsNet.Server.Internal;
+
+///
+/// Maps to OS signal-like behavior.
+/// Mirrors CommandToSignal and ProcessSignal from signal.go.
+/// In .NET, signal sending is replaced by process-level signaling on Unix.
+///
+public static class SignalHandler
+{
+ private static string _processName = "nats-server";
+
+ ///
+ /// Sets the process name used for resolving PIDs.
+ /// Mirrors SetProcessName in signal.go.
+ ///
+ public static void SetProcessName(string name) => _processName = name;
+
+ ///
+ /// Sends a signal command to a running NATS server process.
+ /// On Unix, maps commands to kill signals.
+ /// On Windows, this is a no-op (service manager handles signals).
+ /// Mirrors ProcessSignal in signal.go.
+ ///
+ public static Exception? ProcessSignal(ServerCommand command, string pidExpr = "")
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return new PlatformNotSupportedException("Signal processing not supported on Windows; use service manager.");
+
+ try
+ {
+ List pids;
+ if (string.IsNullOrEmpty(pidExpr))
+ {
+ pids = ResolvePids();
+ if (pids.Count == 0)
+ return new InvalidOperationException("no nats-server processes found");
+ }
+ else
+ {
+ if (int.TryParse(pidExpr, out var pid))
+ pids = [pid];
+ else
+ return new InvalidOperationException($"invalid pid: {pidExpr}");
+ }
+
+ var signal = CommandToUnixSignal(command);
+
+ foreach (var pid in pids)
+ Process.GetProcessById(pid).Kill(signal == UnixSignal.SigKill);
+
+ return null;
+ }
+ catch (Exception ex)
+ {
+ return ex;
+ }
+ }
+
+ ///
+ /// Resolves PIDs of running nats-server processes via pgrep.
+ /// Mirrors resolvePids in signal.go.
+ ///
+ public static List ResolvePids()
+ {
+ var pids = new List();
+ try
+ {
+ var psi = new ProcessStartInfo("pgrep", _processName)
+ {
+ RedirectStandardOutput = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ };
+ using var proc = Process.Start(psi);
+ if (proc == null) return pids;
+
+ var output = proc.StandardOutput.ReadToEnd();
+ proc.WaitForExit();
+
+ var currentPid = Environment.ProcessId;
+ foreach (var line in output.Split('\n', StringSplitOptions.RemoveEmptyEntries))
+ {
+ if (int.TryParse(line.Trim(), out var pid) && pid != currentPid)
+ pids.Add(pid);
+ }
+ }
+ catch
+ {
+ // pgrep not available or failed
+ }
+ return pids;
+ }
+
+ ///
+ /// Maps a server command to Unix signal.
+ /// Mirrors CommandToSignal in signal.go.
+ ///
+ public static UnixSignal CommandToUnixSignal(ServerCommand command) => command switch
+ {
+ ServerCommand.Stop => UnixSignal.SigKill,
+ ServerCommand.Quit => UnixSignal.SigInt,
+ ServerCommand.Reopen => UnixSignal.SigUsr1,
+ ServerCommand.Reload => UnixSignal.SigHup,
+ _ => throw new ArgumentOutOfRangeException(nameof(command), $"unknown command: {command}"),
+ };
+
+ ///
+ /// Runs the server (non-Windows). Mirrors Run in service.go.
+ ///
+ public static void Run(Action startServer) => startServer();
+
+ ///
+ /// Returns false on non-Windows. Mirrors isWindowsService.
+ ///
+ public static bool IsWindowsService() => false;
+}
+
+/// Unix signal codes for NATS command mapping.
+public enum UnixSignal
+{
+ SigInt = 2,
+ SigKill = 9,
+ SigUsr1 = 10,
+ SigHup = 1,
+ SigUsr2 = 12,
+ SigTerm = 15,
+}
diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/NatsLoggerTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/NatsLoggerTests.cs
new file mode 100644
index 0000000..f5466c7
--- /dev/null
+++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/NatsLoggerTests.cs
@@ -0,0 +1,131 @@
+// Copyright 2012-2025 The NATS Authors
+// Licensed under the Apache License, Version 2.0
+
+using Shouldly;
+using ZB.MOM.NatsNet.Server.Internal;
+
+namespace ZB.MOM.NatsNet.Server.Tests.Internal;
+
+///
+/// Tests for NatsLogger / ServerLogging — mirrors tests from server/log_test.go.
+///
+public class NatsLoggerTests
+{
+ private sealed class TestLogger : INatsLogger
+ {
+ public List Messages { get; } = [];
+
+ public void Noticef(string format, params object[] args) => Messages.Add($"[INF] {string.Format(format, args)}");
+ public void Warnf(string format, params object[] args) => Messages.Add($"[WRN] {string.Format(format, args)}");
+ public void Fatalf(string format, params object[] args) => Messages.Add($"[FTL] {string.Format(format, args)}");
+ public void Errorf(string format, params object[] args) => Messages.Add($"[ERR] {string.Format(format, args)}");
+ public void Debugf(string format, params object[] args) => Messages.Add($"[DBG] {string.Format(format, args)}");
+ public void Tracef(string format, params object[] args) => Messages.Add($"[TRC] {string.Format(format, args)}");
+ }
+
+ ///
+ /// Mirrors TestSetLogger — verify logger assignment and atomic flags.
+ ///
+ [Fact] // T:2017
+ public void SetLogger_ShouldSetLoggerAndFlags()
+ {
+ var logging = new ServerLogging();
+ var testLog = new TestLogger();
+
+ logging.SetLoggerV2(testLog, true, true, false);
+ logging.IsDebug.ShouldBeTrue();
+ logging.IsTrace.ShouldBeTrue();
+ logging.IsTraceSysAcc.ShouldBeFalse();
+ logging.GetLogger().ShouldBe(testLog);
+ }
+
+ ///
+ /// Verify all log methods produce output when flags enabled.
+ ///
+ [Fact] // T:2017 (continuation)
+ public void AllLogMethods_ShouldProduceOutput()
+ {
+ var logging = new ServerLogging();
+ var testLog = new TestLogger();
+ logging.SetLoggerV2(testLog, true, true, false);
+
+ logging.Noticef("notice {0}", "test");
+ logging.Errorf("error {0}", "test");
+ logging.Warnf("warn {0}", "test");
+ logging.Fatalf("fatal {0}", "test");
+ logging.Debugf("debug {0}", "test");
+ logging.Tracef("trace {0}", "test");
+
+ testLog.Messages.Count.ShouldBe(6);
+ testLog.Messages[0].ShouldContain("[INF]");
+ testLog.Messages[1].ShouldContain("[ERR]");
+ testLog.Messages[4].ShouldContain("[DBG]");
+ testLog.Messages[5].ShouldContain("[TRC]");
+ }
+
+ ///
+ /// Debug/Trace should not produce output when flags disabled.
+ ///
+ [Fact] // T:2017 (continuation)
+ public void DebugTrace_ShouldBeNoOpWhenDisabled()
+ {
+ var logging = new ServerLogging();
+ var testLog = new TestLogger();
+ logging.SetLoggerV2(testLog, false, false, false);
+
+ logging.Debugf("debug");
+ logging.Tracef("trace");
+
+ testLog.Messages.ShouldBeEmpty();
+ }
+
+ ///
+ /// Verify null logger does not throw.
+ ///
+ [Fact]
+ public void NullLogger_ShouldNotThrow()
+ {
+ var logging = new ServerLogging();
+ Should.NotThrow(() => logging.Noticef("test"));
+ Should.NotThrow(() => logging.Errorf("test"));
+ Should.NotThrow(() => logging.Debugf("test"));
+ }
+
+ ///
+ /// Verify rate-limited logging suppresses duplicate messages.
+ ///
+ [Fact] // T:2017 (RateLimitWarnf behavior)
+ public void RateLimitWarnf_ShouldSuppressDuplicates()
+ {
+ var logging = new ServerLogging();
+ var testLog = new TestLogger();
+ logging.SetLoggerV2(testLog, false, false, false);
+
+ logging.RateLimitWarnf("duplicate message");
+ logging.RateLimitWarnf("duplicate message");
+ logging.RateLimitWarnf("different message");
+
+ // Should only log 2 unique messages, not 3.
+ testLog.Messages.Count.ShouldBe(2);
+ }
+
+ ///
+ /// Verify Errors/Errorc/Errorsc convenience methods.
+ ///
+ [Fact]
+ public void ErrorVariants_ShouldFormatCorrectly()
+ {
+ var logging = new ServerLogging();
+ var testLog = new TestLogger();
+ logging.SetLoggerV2(testLog, false, false, false);
+
+ logging.Errors("client", new Exception("conn reset"));
+ logging.Errorc("TLS", new Exception("cert expired"));
+ logging.Errorsc("route", "cluster", new Exception("timeout"));
+
+ testLog.Messages.Count.ShouldBe(3);
+ testLog.Messages[0].ShouldContain("client - conn reset");
+ testLog.Messages[1].ShouldContain("TLS: cert expired");
+ testLog.Messages[2].ShouldContain("route - cluster: timeout");
+ }
+}
diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/SignalHandlerTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/SignalHandlerTests.cs
new file mode 100644
index 0000000..cfc3400
--- /dev/null
+++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/SignalHandlerTests.cs
@@ -0,0 +1,70 @@
+// Copyright 2012-2025 The NATS Authors
+// Licensed under the Apache License, Version 2.0
+
+using System.Runtime.InteropServices;
+using Shouldly;
+using ZB.MOM.NatsNet.Server.Internal;
+
+namespace ZB.MOM.NatsNet.Server.Tests.Internal;
+
+///
+/// Tests for SignalHandler — mirrors tests from server/signal_test.go.
+///
+public class SignalHandlerTests
+{
+ ///
+ /// Mirrors CommandToSignal mapping tests.
+ ///
+ [Fact] // T:3158
+ public void CommandToUnixSignal_ShouldMapCorrectly()
+ {
+ SignalHandler.CommandToUnixSignal(ServerCommand.Stop).ShouldBe(UnixSignal.SigKill);
+ SignalHandler.CommandToUnixSignal(ServerCommand.Quit).ShouldBe(UnixSignal.SigInt);
+ SignalHandler.CommandToUnixSignal(ServerCommand.Reopen).ShouldBe(UnixSignal.SigUsr1);
+ SignalHandler.CommandToUnixSignal(ServerCommand.Reload).ShouldBe(UnixSignal.SigHup);
+ }
+
+ ///
+ /// Mirrors SetProcessName test.
+ ///
+ [Fact] // T:3155
+ public void SetProcessName_ShouldNotThrow()
+ {
+ Should.NotThrow(() => SignalHandler.SetProcessName("test-server"));
+ }
+
+ ///
+ /// Verify IsWindowsService returns false on non-Windows.
+ ///
+ [Fact] // T:3149
+ public void IsWindowsService_ShouldReturnFalse()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return; // Skip on Windows
+ SignalHandler.IsWindowsService().ShouldBeFalse();
+ }
+
+ ///
+ /// Mirrors Run — service.go Run() simply invokes the start function.
+ ///
+ [Fact] // T:3148
+ public void Run_ShouldInvokeStartAction()
+ {
+ var called = false;
+ SignalHandler.Run(() => called = true);
+ called.ShouldBeTrue();
+ }
+
+ ///
+ /// ProcessSignal with invalid PID expression should return error.
+ ///
+ [Fact] // T:3157
+ public void ProcessSignal_InvalidPid_ShouldReturnError()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return; // Skip on Windows
+
+ var err = SignalHandler.ProcessSignal(ServerCommand.Stop, "not-a-pid");
+ err.ShouldNotBeNull();
+ }
+}
diff --git a/porting.db b/porting.db
index 3c7bf3a..ea5a0b7 100644
Binary files a/porting.db and b/porting.db differ
diff --git a/reports/current.md b/reports/current.md
index 9062e7b..9a95428 100644
--- a/reports/current.md
+++ b/reports/current.md
@@ -1,6 +1,6 @@
# NATS .NET Porting Status Report
-Generated: 2026-02-26 16:51:01 UTC
+Generated: 2026-02-26 16:54:25 UTC
## Modules (12 total)
@@ -13,19 +13,19 @@ Generated: 2026-02-26 16:51:01 UTC
| Status | Count |
|--------|-------|
-| complete | 344 |
+| complete | 368 |
| n_a | 82 |
-| not_started | 3180 |
-| stub | 67 |
+| not_started | 3155 |
+| stub | 68 |
## Unit Tests (3257 total)
| Status | Count |
|--------|-------|
-| complete | 148 |
-| n_a | 49 |
-| not_started | 2980 |
-| stub | 80 |
+| complete | 155 |
+| n_a | 50 |
+| not_started | 2953 |
+| stub | 99 |
## Library Mappings (36 total)
@@ -36,4 +36,4 @@ Generated: 2026-02-26 16:51:01 UTC
## Overall Progress
-**634/6942 items complete (9.1%)**
+**666/6942 items complete (9.6%)**
diff --git a/reports/report_f08fc5d.md b/reports/report_f08fc5d.md
new file mode 100644
index 0000000..9a95428
--- /dev/null
+++ b/reports/report_f08fc5d.md
@@ -0,0 +1,39 @@
+# NATS .NET Porting Status Report
+
+Generated: 2026-02-26 16:54:25 UTC
+
+## Modules (12 total)
+
+| Status | Count |
+|--------|-------|
+| complete | 11 |
+| not_started | 1 |
+
+## Features (3673 total)
+
+| Status | Count |
+|--------|-------|
+| complete | 368 |
+| n_a | 82 |
+| not_started | 3155 |
+| stub | 68 |
+
+## Unit Tests (3257 total)
+
+| Status | Count |
+|--------|-------|
+| complete | 155 |
+| n_a | 50 |
+| not_started | 2953 |
+| stub | 99 |
+
+## Library Mappings (36 total)
+
+| Status | Count |
+|--------|-------|
+| mapped | 36 |
+
+
+## Overall Progress
+
+**666/6942 items complete (9.6%)**