diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Config/NatsJsonConverters.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Config/NatsJsonConverters.cs
new file mode 100644
index 0000000..5bc453a
--- /dev/null
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/Config/NatsJsonConverters.cs
@@ -0,0 +1,169 @@
+// 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 parse utility functions in server/opts.go in the NATS server Go source.
+
+using System.Net.Security;
+using System.Security.Authentication;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.RegularExpressions;
+
+namespace ZB.MOM.NatsNet.Server.Config;
+
+///
+/// Converts NATS duration strings (e.g. "2s", "100ms", "1h30m") to .
+/// Mirrors Go parseDuration in server/opts.go.
+///
+public sealed class NatsDurationJsonConverter : JsonConverter
+{
+ private static readonly Regex Pattern = new(
+ @"^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?(?:(\d+)ms)?(?:(\d+)us)?(?:(\d+)ns)?$",
+ RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var raw = reader.GetString() ?? throw new JsonException("Expected a duration string");
+ return Parse(raw);
+ }
+
+ public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
+ => writer.WriteStringValue(FormatDuration(value));
+
+ ///
+ /// Parses a NATS-style duration string. Accepts Go time.Duration format strings and ISO 8601.
+ ///
+ public static TimeSpan Parse(string s)
+ {
+ if (string.IsNullOrWhiteSpace(s))
+ throw new FormatException("Duration string is empty");
+
+ // Try Go-style: e.g. "2s", "100ms", "1h30m", "5m10s"
+ var m = Pattern.Match(s);
+ if (m.Success && m.Value.Length > 0)
+ {
+ var hours = m.Groups[1].Success ? int.Parse(m.Groups[1].Value) : 0;
+ var minutes = m.Groups[2].Success ? int.Parse(m.Groups[2].Value) : 0;
+ var seconds = m.Groups[3].Success ? int.Parse(m.Groups[3].Value) : 0;
+ var ms = m.Groups[4].Success ? int.Parse(m.Groups[4].Value) : 0;
+ var us = m.Groups[5].Success ? int.Parse(m.Groups[5].Value) : 0;
+ var ns = m.Groups[6].Success ? int.Parse(m.Groups[6].Value) : 0;
+ return new TimeSpan(0, hours, minutes, seconds, ms)
+ + TimeSpan.FromMicroseconds(us)
+ + TimeSpan.FromTicks(ns / 100); // 1 tick = 100 ns
+ }
+
+ // Try .NET TimeSpan.Parse (handles "hh:mm:ss")
+ if (TimeSpan.TryParse(s, out var ts)) return ts;
+
+ throw new FormatException($"Cannot parse duration string: \"{s}\"");
+ }
+
+ private static string FormatDuration(TimeSpan ts)
+ {
+ if (ts.TotalMilliseconds < 1) return $"{(long)ts.TotalNanoseconds}ns";
+ if (ts.TotalSeconds < 1) return $"{(long)ts.TotalMilliseconds}ms";
+ if (ts.TotalMinutes < 1) return $"{(long)ts.TotalSeconds}s";
+ if (ts.TotalHours < 1) return $"{ts.Minutes}m{ts.Seconds}s";
+ return $"{(int)ts.TotalHours}h{ts.Minutes}m{ts.Seconds}s";
+ }
+}
+
+///
+/// Converts a TLS version string ("1.2", "1.3", "TLS12") to .
+/// Mirrors Go parseTLSVersion in server/opts.go.
+///
+public sealed class TlsVersionJsonConverter : JsonConverter
+{
+ public override SslProtocols Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var raw = reader.GetString()?.Trim() ?? string.Empty;
+ return Parse(raw);
+ }
+
+ public override void Write(Utf8JsonWriter writer, SslProtocols value, JsonSerializerOptions options)
+ => writer.WriteStringValue(value.ToString());
+
+ public static SslProtocols Parse(string s) => s.ToUpperInvariant() switch
+ {
+ "1.2" or "TLS12" or "TLSV1.2" => SslProtocols.Tls12,
+ "1.3" or "TLS13" or "TLSV1.3" => SslProtocols.Tls13,
+ _ => throw new FormatException($"Unknown TLS version: \"{s}\""),
+ };
+}
+
+///
+/// Validates and normalises a NATS URL string (nats://host:port).
+/// Mirrors Go parseURL in server/opts.go.
+///
+public sealed class NatsUrlJsonConverter : JsonConverter
+{
+ public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var raw = reader.GetString() ?? string.Empty;
+ return Normalise(raw);
+ }
+
+ public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
+ => writer.WriteStringValue(value);
+
+ public static string Normalise(string url)
+ {
+ if (string.IsNullOrWhiteSpace(url)) return url;
+ url = url.Trim();
+ if (!url.Contains("://")) url = "nats://" + url;
+ if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
+ throw new FormatException($"Invalid NATS URL: \"{url}\"");
+ return uri.ToString().TrimEnd('/');
+ }
+}
+
+///
+/// Converts a storage size string ("1GB", "512MB", "1024") to a byte count (long).
+/// Mirrors Go getStorageSize in server/opts.go.
+///
+public sealed class StorageSizeJsonConverter : JsonConverter
+{
+ private static readonly Regex Pattern = new(@"^(\d+(?:\.\d+)?)\s*([KMGT]?B?)?$",
+ RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.Number)
+ {
+ return reader.GetInt64();
+ }
+ var raw = reader.GetString() ?? "0";
+ return Parse(raw);
+ }
+
+ public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options)
+ => writer.WriteNumberValue(value);
+
+ public static long Parse(string s)
+ {
+ if (long.TryParse(s, out var n)) return n;
+ var m = Pattern.Match(s.Trim());
+ if (!m.Success) throw new FormatException($"Invalid storage size: \"{s}\"");
+ var num = double.Parse(m.Groups[1].Value);
+ var suffix = m.Groups[2].Value.ToUpperInvariant();
+ return suffix switch
+ {
+ "K" or "KB" => (long)(num * 1024),
+ "M" or "MB" => (long)(num * 1024 * 1024),
+ "G" or "GB" => (long)(num * 1024 * 1024 * 1024),
+ "T" or "TB" => (long)(num * 1024L * 1024 * 1024 * 1024),
+ _ => (long)num,
+ };
+ }
+}
diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Config/ServerOptionsConfiguration.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Config/ServerOptionsConfiguration.cs
new file mode 100644
index 0000000..b6d03f5
--- /dev/null
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/Config/ServerOptionsConfiguration.cs
@@ -0,0 +1,127 @@
+// 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/opts.go in the NATS server Go source.
+
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Extensions.Configuration;
+
+namespace ZB.MOM.NatsNet.Server.Config;
+
+///
+/// Loads and binds NATS server configuration from an appsettings.json file.
+/// Replaces the Go processConfigFile / processConfigFileLine pipeline
+/// and all parse* helper functions in server/opts.go.
+///
+public static class ServerOptionsConfiguration
+{
+ ///
+ /// Creates a instance pre-configured with all
+ /// NATS-specific JSON converters.
+ ///
+ public static JsonSerializerOptions CreateJsonOptions()
+ {
+ var opts = new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true,
+ AllowTrailingCommas = true,
+ ReadCommentHandling = JsonCommentHandling.Skip,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ };
+ opts.Converters.Add(new NatsDurationJsonConverter());
+ opts.Converters.Add(new TlsVersionJsonConverter());
+ opts.Converters.Add(new StorageSizeJsonConverter());
+ return opts;
+ }
+
+ ///
+ /// Reads a JSON file at and returns a bound
+ /// instance.
+ /// Mirrors Go ProcessConfigFile and Options.ProcessConfigFile.
+ ///
+ public static ServerOptions ProcessConfigFile(string path)
+ {
+ if (!File.Exists(path))
+ throw new FileNotFoundException($"Configuration file not found: {path}", path);
+
+ var json = File.ReadAllText(path, Encoding.UTF8);
+ return ProcessConfigString(json);
+ }
+
+ ///
+ /// Deserialises a JSON string and returns a bound instance.
+ /// Mirrors Go Options.ProcessConfigString.
+ ///
+ public static ServerOptions ProcessConfigString(string json)
+ {
+ ArgumentNullException.ThrowIfNullOrEmpty(json);
+ var opts = JsonSerializer.Deserialize(json, CreateJsonOptions())
+ ?? new ServerOptions();
+ PostProcess(opts);
+ return opts;
+ }
+
+ ///
+ /// Binds a pre-built (e.g. from an ASP.NET Core host)
+ /// to a instance.
+ /// The configuration section should be the root or a named section such as "NatsServer".
+ ///
+ public static void BindConfiguration(IConfiguration config, ServerOptions target)
+ {
+ ArgumentNullException.ThrowIfNull(config);
+ ArgumentNullException.ThrowIfNull(target);
+ config.Bind(target);
+ PostProcess(target);
+ }
+
+ // -------------------------------------------------------------------------
+ // Post-processing
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Applies defaults and cross-field validation after loading.
+ /// Mirrors the end of Options.processConfigFile and
+ /// configureSystemAccount in server/opts.go.
+ ///
+ private static void PostProcess(ServerOptions opts)
+ {
+ // Apply default port if not set.
+ if (opts.Port == 0) opts.Port = ServerConstants.DefaultPort;
+
+ // Apply default host if not set.
+ if (string.IsNullOrEmpty(opts.Host)) opts.Host = ServerConstants.DefaultHost;
+
+ // Apply default max payload.
+ if (opts.MaxPayload == 0) opts.MaxPayload = ServerConstants.MaxPayload;
+
+ // Apply default auth timeout.
+ if (opts.AuthTimeout == 0) opts.AuthTimeout = ServerConstants.DefaultAuthTimeout;
+
+ // Ensure SystemAccount defaults if not set.
+ ConfigureSystemAccount(opts);
+ }
+
+ ///
+ /// Sets up the system account name from options.
+ /// Mirrors Go configureSystemAccount in server/opts.go.
+ ///
+ private static void ConfigureSystemAccount(ServerOptions opts)
+ {
+ // If system account already set, nothing to do.
+ if (!string.IsNullOrEmpty(opts.SystemAccount)) return;
+ // Default to "$SYS" if not explicitly disabled.
+ opts.SystemAccount = ServerConstants.DefaultSystemAccount;
+ }
+}
diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ServerConstants.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ServerConstants.cs
index 40e8a45..7fca879 100644
--- a/dotnet/src/ZB.MOM.NatsNet.Server/ServerConstants.cs
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/ServerConstants.cs
@@ -63,6 +63,12 @@ public static class ServerConstants
// Auth timeout — mirrors AUTH_TIMEOUT.
public static readonly TimeSpan AuthTimeout = TimeSpan.FromSeconds(2);
+ // Default auth timeout as a double (seconds) — used by ServerOptions.AuthTimeout.
+ public const double DefaultAuthTimeout = 2.0;
+
+ // Maximum payload size alias used by config binding — mirrors MAX_PAYLOAD_SIZE.
+ public const int MaxPayload = MaxPayloadSize;
+
// How often pings are sent — mirrors DEFAULT_PING_INTERVAL.
public static readonly TimeSpan DefaultPingInterval = TimeSpan.FromMinutes(2);
diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.cs
index 5835819..47bbaf9 100644
--- a/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.cs
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.cs
@@ -15,6 +15,7 @@
using System.Net.Security;
using System.Security.Authentication;
+using System.Text.Json.Serialization;
using System.Threading;
using ZB.MOM.NatsNet.Server.Auth;
@@ -31,20 +32,28 @@ public sealed partial class ServerOptions
// -------------------------------------------------------------------------
public string ConfigFile { get; set; } = string.Empty;
+ [JsonPropertyName("server_name")]
public string ServerName { get; set; } = string.Empty;
+ [JsonPropertyName("host")]
public string Host { get; set; } = string.Empty;
+ [JsonPropertyName("port")]
public int Port { get; set; }
public bool DontListen { get; set; }
+ [JsonPropertyName("client_advertise")]
public string ClientAdvertise { get; set; } = string.Empty;
public bool CheckConfig { get; set; }
+ [JsonPropertyName("pid_file")]
public string PidFile { get; set; } = string.Empty;
+ [JsonPropertyName("ports_file_dir")]
public string PortsFileDir { get; set; } = string.Empty;
// -------------------------------------------------------------------------
// Logging & Debugging
// -------------------------------------------------------------------------
+ [JsonPropertyName("trace")]
public bool Trace { get; set; }
+ [JsonPropertyName("debug")]
public bool Debug { get; set; }
public bool TraceVerbose { get; set; }
public bool TraceHeaders { get; set; }
@@ -52,7 +61,9 @@ public sealed partial class ServerOptions
public bool NoSigs { get; set; }
public bool Logtime { get; set; }
public bool LogtimeUtc { get; set; }
+ [JsonPropertyName("logfile")]
public string LogFile { get; set; } = string.Empty;
+ [JsonPropertyName("log_size_limit")]
public long LogSizeLimit { get; set; }
public long LogMaxFiles { get; set; }
public bool Syslog { get; set; }
@@ -65,11 +76,14 @@ public sealed partial class ServerOptions
// Networking & Limits
// -------------------------------------------------------------------------
+ [JsonPropertyName("max_connections")]
public int MaxConn { get; set; }
public int MaxSubs { get; set; }
public byte MaxSubTokens { get; set; }
public int MaxControlLine { get; set; }
+ [JsonPropertyName("max_payload")]
public int MaxPayload { get; set; }
+ [JsonPropertyName("max_pending")]
public long MaxPending { get; set; }
public bool NoFastProducerStall { get; set; }
public bool ProxyRequired { get; set; }
@@ -80,11 +94,16 @@ public sealed partial class ServerOptions
// Connectivity
// -------------------------------------------------------------------------
+ [JsonPropertyName("ping_interval")]
public TimeSpan PingInterval { get; set; }
+ [JsonPropertyName("ping_max")]
public int MaxPingsOut { get; set; }
+ [JsonPropertyName("write_deadline")]
public TimeSpan WriteDeadline { get; set; }
public WriteTimeoutPolicy WriteTimeout { get; set; }
+ [JsonPropertyName("lame_duck_duration")]
public TimeSpan LameDuckDuration { get; set; }
+ [JsonPropertyName("lame_duck_grace_period")]
public TimeSpan LameDuckGracePeriod { get; set; }
// -------------------------------------------------------------------------
@@ -92,23 +111,33 @@ public sealed partial class ServerOptions
// -------------------------------------------------------------------------
public string HttpHost { get; set; } = string.Empty;
+ [JsonPropertyName("http_port")]
public int HttpPort { get; set; }
+ [JsonPropertyName("http_base_path")]
public string HttpBasePath { get; set; } = string.Empty;
+ [JsonPropertyName("https_port")]
public int HttpsPort { get; set; }
// -------------------------------------------------------------------------
// Authentication & Authorization
// -------------------------------------------------------------------------
+ [JsonPropertyName("username")]
public string Username { get; set; } = string.Empty;
+ [JsonPropertyName("password")]
public string Password { get; set; } = string.Empty;
+ [JsonPropertyName("authorization")]
public string Authorization { get; set; } = string.Empty;
+ [JsonPropertyName("auth_timeout")]
public double AuthTimeout { get; set; }
+ [JsonPropertyName("no_auth_user")]
public string NoAuthUser { get; set; } = string.Empty;
public string DefaultSentinel { get; set; } = string.Empty;
+ [JsonPropertyName("system_account")]
public string SystemAccount { get; set; } = string.Empty;
public bool NoSystemAccount { get; set; }
/// Parsed account objects from config. Mirrors Go opts.Accounts.
+ [JsonPropertyName("accounts")]
public List Accounts { get; set; } = [];
public AuthCalloutOpts? AuthCallout { get; set; }
public bool AlwaysEnableNonce { get; set; }
@@ -148,8 +177,11 @@ public sealed partial class ServerOptions
// Cluster / Gateway / Leaf / WebSocket / MQTT
// -------------------------------------------------------------------------
+ [JsonPropertyName("cluster")]
public ClusterOpts Cluster { get; set; } = new();
+ [JsonPropertyName("gateway")]
public GatewayOpts Gateway { get; set; } = new();
+ [JsonPropertyName("leafnodes")]
public LeafNodeOpts LeafNode { get; set; } = new();
public WebsocketOpts Websocket { get; set; } = new();
public MqttOpts Mqtt { get; set; } = new();
@@ -165,6 +197,7 @@ public sealed partial class ServerOptions
// JetStream
// -------------------------------------------------------------------------
+ [JsonPropertyName("jetstream")]
public bool JetStream { get; set; }
public bool NoJetStreamStrict { get; set; }
public long JetStreamMaxMemory { get; set; }
@@ -184,6 +217,7 @@ public sealed partial class ServerOptions
public bool JetStreamMetaCompactSync { get; set; }
public int StreamMaxBufferedMsgs { get; set; }
public long StreamMaxBufferedSize { get; set; }
+ [JsonPropertyName("store_dir")]
public string StoreDir { get; set; } = string.Empty;
public TimeSpan SyncInterval { get; set; }
public bool SyncAlways { get; set; }
diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ZB.MOM.NatsNet.Server.csproj b/dotnet/src/ZB.MOM.NatsNet.Server/ZB.MOM.NatsNet.Server.csproj
index 04c1046..c11f5bf 100644
--- a/dotnet/src/ZB.MOM.NatsNet.Server/ZB.MOM.NatsNet.Server.csproj
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/ZB.MOM.NatsNet.Server.csproj
@@ -9,6 +9,8 @@
+
+
diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Config/ServerOptionsConfigurationTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Config/ServerOptionsConfigurationTests.cs
new file mode 100644
index 0000000..1a0fcde
--- /dev/null
+++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Config/ServerOptionsConfigurationTests.cs
@@ -0,0 +1,111 @@
+// Copyright 2012-2025 The NATS Authors
+// Licensed under the Apache License, Version 2.0
+namespace ZB.MOM.NatsNet.Server.Tests.Config;
+
+using ZB.MOM.NatsNet.Server.Config;
+using Shouldly;
+using Xunit;
+
+public class ServerOptionsConfigurationTests
+{
+ [Fact]
+ public void ProcessConfigString_MinimalJson_SetsPort()
+ {
+ var opts = ServerOptionsConfiguration.ProcessConfigString("""{"port": 4222}""");
+ opts.Port.ShouldBe(4222);
+ }
+
+ [Fact]
+ public void ProcessConfigString_EmptyJson_AppliesDefaults()
+ {
+ var opts = ServerOptionsConfiguration.ProcessConfigString("{}");
+ opts.Port.ShouldBe(ServerConstants.DefaultPort);
+ opts.Host.ShouldBe(ServerConstants.DefaultHost);
+ opts.MaxPayload.ShouldBe(ServerConstants.MaxPayload);
+ }
+
+ [Fact]
+ public void ProcessConfigString_AllBasicFields_Roundtrip()
+ {
+ var json = """
+ {
+ "port": 5222,
+ "host": "127.0.0.1",
+ "server_name": "test-server",
+ "debug": true,
+ "trace": true,
+ "max_connections": 100,
+ "auth_timeout": 2.0
+ }
+ """;
+ var opts = ServerOptionsConfiguration.ProcessConfigString(json);
+ opts.Port.ShouldBe(5222);
+ opts.Host.ShouldBe("127.0.0.1");
+ opts.ServerName.ShouldBe("test-server");
+ opts.Debug.ShouldBeTrue();
+ opts.Trace.ShouldBeTrue();
+ opts.MaxConn.ShouldBe(100);
+ opts.AuthTimeout.ShouldBe(2.0);
+ }
+
+ [Fact]
+ public void ProcessConfigFile_FileNotFound_Throws()
+ {
+ Should.Throw(() =>
+ ServerOptionsConfiguration.ProcessConfigFile("/nonexistent/path.json"));
+ }
+
+ [Fact]
+ public void ProcessConfigFile_ValidFile_ReturnsOptions()
+ {
+ var tmpFile = Path.GetTempFileName();
+ File.WriteAllText(tmpFile, """{"port": 9090, "server_name": "from-file"}""");
+ try
+ {
+ var opts = ServerOptionsConfiguration.ProcessConfigFile(tmpFile);
+ opts.Port.ShouldBe(9090);
+ opts.ServerName.ShouldBe("from-file");
+ }
+ finally { File.Delete(tmpFile); }
+ }
+}
+
+public class NatsDurationJsonConverterTests
+{
+ [Theory]
+ [InlineData("2s", 0, 0, 2, 0)]
+ [InlineData("100ms", 0, 0, 0, 100)]
+ [InlineData("1h30m", 1, 30, 0, 0)]
+ public void Parse_ValidDurationStrings_ReturnsCorrectTimeSpan(
+ string input, int hours, int minutes, int seconds, int ms)
+ {
+ var expected = new TimeSpan(0, hours, minutes, seconds, ms);
+ NatsDurationJsonConverter.Parse(input).ShouldBe(expected);
+ }
+
+ [Fact]
+ public void Parse_FiveMinutesTenSeconds_ReturnsCorrectSpan()
+ {
+ var result = NatsDurationJsonConverter.Parse("5m10s");
+ result.ShouldBe(TimeSpan.FromSeconds(310));
+ }
+
+ [Fact]
+ public void Parse_InvalidString_ThrowsFormatException()
+ {
+ Should.Throw(() => NatsDurationJsonConverter.Parse("notaduration"));
+ }
+}
+
+public class StorageSizeJsonConverterTests
+{
+ [Theory]
+ [InlineData("1GB", 1L * 1024 * 1024 * 1024)]
+ [InlineData("512MB", 512L * 1024 * 1024)]
+ [InlineData("1KB", 1024L)]
+ [InlineData("1024", 1024L)]
+ public void Parse_ValidSizeStrings_ReturnsBytes(string input, long expectedBytes)
+ {
+ StorageSizeJsonConverter.Parse(input).ShouldBe(expectedBytes);
+ }
+}
diff --git a/porting.db b/porting.db
index 78f31b3..9984857 100644
Binary files a/porting.db and b/porting.db differ
diff --git a/reports/current.md b/reports/current.md
index 5f519ce..0a855a4 100644
--- a/reports/current.md
+++ b/reports/current.md
@@ -1,6 +1,6 @@
# NATS .NET Porting Status Report
-Generated: 2026-02-26 21:59:34 UTC
+Generated: 2026-02-26 22:18:29 UTC
## Modules (12 total)
@@ -13,9 +13,9 @@ Generated: 2026-02-26 21:59:34 UTC
| Status | Count |
|--------|-------|
-| complete | 3503 |
+| complete | 3570 |
| n_a | 77 |
-| stub | 93 |
+| stub | 26 |
## Unit Tests (3257 total)
@@ -35,4 +35,4 @@ Generated: 2026-02-26 21:59:34 UTC
## Overall Progress
-**4091/6942 items complete (58.9%)**
+**4158/6942 items complete (59.9%)**
diff --git a/reports/report_8253f97.md b/reports/report_8253f97.md
new file mode 100644
index 0000000..0a855a4
--- /dev/null
+++ b/reports/report_8253f97.md
@@ -0,0 +1,38 @@
+# NATS .NET Porting Status Report
+
+Generated: 2026-02-26 22:18:29 UTC
+
+## Modules (12 total)
+
+| Status | Count |
+|--------|-------|
+| complete | 11 |
+| not_started | 1 |
+
+## Features (3673 total)
+
+| Status | Count |
+|--------|-------|
+| complete | 3570 |
+| n_a | 77 |
+| stub | 26 |
+
+## Unit Tests (3257 total)
+
+| Status | Count |
+|--------|-------|
+| complete | 319 |
+| n_a | 181 |
+| not_started | 2533 |
+| stub | 224 |
+
+## Library Mappings (36 total)
+
+| Status | Count |
+|--------|-------|
+| mapped | 36 |
+
+
+## Overall Progress
+
+**4158/6942 items complete (59.9%)**