# Routes — Gap Analysis > This file tracks what has and hasn't been ported from Go to .NET for the **Routes** module. > See [stillmissing.md](stillmissing.md) for the full LOC comparison across all modules. ## LLM Instructions: How to Analyze This Category ### Step 1: Read the Go Reference Files Read each Go source file listed below. For every file: 1. Extract all **exported types** (structs, interfaces, type aliases) 2. Extract all **exported methods** on those types (receiver functions) 3. Extract all **exported standalone functions** 4. Note **key constants, enums, and protocol states** 5. Note **important unexported helpers** that implement core logic (functions >20 lines) 6. Pay attention to **concurrency patterns** (goroutines, mutexes, channels) — these map to different .NET patterns ### Step 2: Read the .NET Implementation Files Read all `.cs` files in the .NET directories listed below. For each Go symbol found in Step 1: 1. Search for a matching type, method, or function in .NET 2. If found, compare the behavior: does it handle the same edge cases? Same error paths? 3. If partially implemented, note what's missing 4. If not found, note it as MISSING ### Step 3: Cross-Reference Tests Compare Go test functions against .NET test methods: 1. For each Go `Test*` function, check if a corresponding .NET `[Fact]` or `[Theory]` exists 2. Note which test scenarios are covered and which are missing 3. Check the parity DB (`docs/test_parity.db`) for existing mappings: ```bash sqlite3 docs/test_parity.db "SELECT go_test, dotnet_test, confidence FROM test_mappings tm JOIN go_tests gt ON tm.go_test_id=gt.rowid JOIN dotnet_tests dt ON tm.dotnet_test_id=dt.rowid WHERE gt.go_file LIKE '%PATTERN%'" ``` ### Step 4: Classify Each Item Use these status values: | Status | Meaning | |--------|---------| | **PORTED** | Equivalent exists in .NET with matching behavior | | **PARTIAL** | .NET implementation exists but is incomplete (missing edge cases, error handling, or features) | | **MISSING** | No .NET equivalent found — needs to be ported | | **NOT_APPLICABLE** | Go-specific pattern that doesn't apply to .NET (build tags, platform-specific goroutine tricks, etc.) | | **DEFERRED** | Intentionally skipped for now (document why) | ### Step 5: Fill In the Gap Inventory Add rows to the Gap Inventory table below. Group by Go source file. Include the Go file and line number so a porting LLM can jump directly to the reference implementation. ### Key Porting Notes for Routes - Route connections use the same `client.go` read/write loop but with `ClientKind = ROUTER`. - Route pooling sends different accounts over different connections to avoid head-of-line blocking. - `RS+`/`RS-` are subscription interest propagation messages between clustered servers. - `RMSG` is the routed message format (differs from client `MSG`). --- ## Go Reference Files (Source) - `golang/nats-server/server/route.go` — Full-mesh cluster routes (~3,300 lines). Route pooling (default 3 connections per peer). Account-specific dedicated routes. Protocol: `RS+`/`RS-` for subscribe propagation, `RMSG` for routed messages. ## Go Reference Files (Tests) - `golang/nats-server/server/routes_test.go` - `golang/nats-server/test/routes_test.go` (integration) - `golang/nats-server/test/new_routes_test.go` (integration) ## .NET Implementation Files (Source) - `src/NATS.Server/Routes/` (all files) ## .NET Implementation Files (Tests) - `tests/NATS.Server.Tests/Routes/` --- ## Gap Inventory ### `golang/nats-server/server/route.go` | Go Symbol | Go File:Line | Status | .NET Equivalent | Notes | |-----------|:-------------|--------|:----------------|-------| | `RouteType` (type alias + consts `Implicit`/`Explicit`) | route.go:36–44 | PORTED | `src/NATS.Server/Routes/RouteConnection.cs` — implicit/explicit distinction tracked in `RouteConnection` handshake and `RouteManager.ConnectToRouteWithRetryAsync` | No explicit enum; Implicit/Explicit distinction is encoded in how routes are established (solicited vs inbound) | | `route` struct (unexported) | route.go:56–94 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:8` | Fields `remoteID`, `poolIdx`, `accName`, `noPool`, `compression`, `gossipMode` are present. Fields for `lnoc`, `lnocu`, `jetstream`, `connectURLs`, `wsConnURLs`, `gatewayURL`, `leafnodeURL`, `hash`, `idHash`, `startNewRoute`, `retry` are MISSING — not modelled in .NET | | `routeInfo` struct (unexported) | route.go:97–101 | MISSING | — | Used internally for deferred pool-connection creation after first PONG; no .NET equivalent | | `gossipDefault`/`gossipDisabled`/`gossipOverride` consts | route.go:104–108 | MISSING | — | Gossip mode bytes used in INFO propagation; not implemented in .NET | | `connectInfo` struct | route.go:110–124 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:328` (`BuildConnectInfoJson`) | .NET builds a simplified JSON payload; `connectInfo` fields Echo, Verbose, Pedantic, TLS, Headers, Cluster, Dynamic, LNOC, LNOCU are all MISSING from the .NET payload | | `ConProto`/`InfoProto` protocol format strings | route.go:127–130 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:73,83` | Handshake uses a simplified `ROUTE ` format rather than `CONNECT ` / `INFO ` | | `clusterTLSInsecureWarning` const | route.go:134 | NOT_APPLICABLE | — | TLS not yet implemented in .NET port; warning string has no counterpart | | `defaultRouteMaxPingInterval` const | route.go:140 | MISSING | — | Ping interval management for compression RTT auto-mode not implemented | | `routeConnectDelay`/`routeConnectMaxDelay`/`routeMaxPingInterval` vars | route.go:145–148 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:486` (250ms hardcoded delay) | .NET hardcodes 250ms retry delay; Go uses configurable `DEFAULT_ROUTE_CONNECT` with exponential backoff | | `(c *client) removeReplySub` | route.go:151 | MISSING | — | Reply-sub cleanup for remote reply subs not implemented | | `(c *client) processAccountSub` | route.go:167 | NOT_APPLICABLE | — | Gateway-only path; gateway sub interest not in routes module | | `(c *client) processAccountUnsub` | route.go:174 | NOT_APPLICABLE | — | Gateway-only path | | `(c *client) processRoutedOriginClusterMsgArgs` | route.go:182 | MISSING | — | LMSG (origin-cluster routed msg) arg parsing not implemented; .NET only handles RMSG | | `(c *client) processRoutedHeaderMsgArgs` | route.go:281 | MISSING | — | HMSG (header msg from route) arg parsing not implemented | | `(c *client) processRoutedMsgArgs` | route.go:378 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:187–222` (`ReadFramesAsync`) | .NET parses basic RMSG account/subject/reply/size. Missing: queue-group routing indicators (`+`/`|`), header size field, multi-field arg parsing | | `(c *client) processInboundRoutedMsg` | route.go:460 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:219–221` | .NET fires `RoutedMessageReceived` callback; missing: stats update, gateway reply handling (`handleGatewayReply`), account lookup and fanout via `processMsgResults` | | `(c *client) sendRouteConnect` | route.go:503 | MISSING | — | Outbound CONNECT protocol (with cluster auth, TLS flags, etc.) not sent; .NET uses a simpler `ROUTE ` handshake | | `computeRoutePoolIdx` | route.go:538 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:149` (`ComputeRoutePoolIdx`) | FNV-1a 32-bit hash, identical algorithm | | `(c *client) processRouteInfo` | route.go:549 | MISSING | — | Full INFO processing (cluster name negotiation, compression negotiation, duplicate detection, account route setup) not implemented; .NET handshake is a simple ID exchange | | `(s *Server) negotiateRouteCompression` | route.go:897 | PARTIAL | `src/NATS.Server/Routes/RouteCompressionCodec.cs:82` (`NegotiateCompression`) | .NET has the negotiation logic; but integration into handshake (INFO exchange, switching compression writer/reader mid-stream) is MISSING | | `(s *Server) updateRemoteRoutePerms` | route.go:953 | MISSING | — | Route permission update on INFO reload not implemented | | `(s *Server) sendAsyncInfoToClients` | route.go:1015 | MISSING | — | Async INFO broadcast to connected clients not implemented | | `(s *Server) processImplicitRoute` | route.go:1043 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:107` (`ProcessImplicitRoute`) | .NET collects discovered URLs; missing: duplicate-ID check, pinned-account re-solicitation, `hasThisRouteConfigured` guard | | `(s *Server) hasThisRouteConfigured` | route.go:1104 | MISSING | — | Check whether incoming gossip URL is already a configured explicit route; not implemented | | `(s *Server) forwardNewRouteInfoToKnownServers` | route.go:1139 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:127` (`ForwardNewRouteInfoToKnownServers`) | .NET raises an event with the new peer URL; missing: gossip mode logic (`gossipDefault`/`gossipDisabled`/`gossipOverride`), pinned-account route filtering, serialized INFO JSON sending | | `(c *client) canImport` | route.go:1226 | MISSING | — | Route import permission check not implemented | | `(c *client) canExport` | route.go:1235 | MISSING | — | Route export permission check not implemented | | `(c *client) setRoutePermissions` | route.go:1244 | MISSING | — | Route permission mapping (Import→Publish, Export→Subscribe) not implemented | | `asubs` struct | route.go:1263 | NOT_APPLICABLE | — | Internal Go helper to group subscriptions by account during cleanup; .NET uses LINQ equivalents | | `getAccNameFromRoutedSubKey` | route.go:1273 | MISSING | — | Sub key parsing for account name extraction not implemented | | `(c *client) getRoutedSubKeyInfo` | route.go:1290 | MISSING | — | Helper to determine account/key info for a route's subscriptions; not implemented | | `(c *client) removeRemoteSubs` | route.go:1299 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:577` (`RemoveRoute`) | .NET removes the route connection but does NOT remove individual remote subscriptions from the SubList on close | | `(c *client) removeRemoteSubsForAcc` | route.go:1352 | MISSING | — | Per-account remote sub removal for dedicated route transition not implemented | | `(c *client) parseUnsubProto` | route.go:1366 | MISSING | — | RS-/LS- protocol arg parser not implemented; .NET `ReadFramesAsync` only extracts account/subject/queue loosely | | `(c *client) processRemoteUnsub` | route.go:1404 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:177–185` | .NET fires `RemoteSubscriptionReceived` with `IsRemoval=true`; missing: sub key lookup and removal from SubList, gateway/leafnode interest updates | | `(c *client) processRemoteSub` | route.go:1489 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:167–175` | .NET fires `RemoteSubscriptionReceived`; missing: key construction with type byte prefix, account lookup/creation, permission check (`canExport`), SubList insertion, gateway/leafnode updates, queue-weight delta tracking | | `(c *client) addRouteSubOrUnsubProtoToBuf` | route.go:1729 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:95–109` (`SendRsPlusAsync`/`SendRsMinusAsync`) | .NET sends RS+/RS- with account and optional queue; missing: LS+/LS- variant for leaf origin clusters, queue weight field in RS+ | | `(s *Server) sendSubsToRoute` | route.go:1781 | MISSING | — | Bulk send of local subscription interest to newly connected route not implemented; .NET only propagates incremental sub/unsub | | `(c *client) sendRouteSubProtos` | route.go:1881 | MISSING | — | Batch RS+ send not implemented | | `(c *client) sendRouteUnSubProtos` | route.go:1890 | MISSING | — | Batch RS- send not implemented | | `(c *client) sendRouteSubOrUnSubProtos` | route.go:1898 | MISSING | — | Low-level batch RS+/RS-/LS+/LS- sender not implemented | | `(s *Server) createRoute` | route.go:1935 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:447,462` (`HandleInboundRouteAsync`/`ConnectToRouteWithRetryAsync`) | .NET creates a RouteConnection and performs handshake; missing: TLS setup, auth timeout timer, CONNECT protocol sending, INFO JSON sending, compression negotiation, ping timer | | `routeShouldDelayInfo` | route.go:2082 | MISSING | — | Logic to delay initial INFO until pool connection auth is confirmed not implemented | | `(s *Server) generateRouteInitialInfoJSON` | route.go:2090 | MISSING | — | Route INFO JSON generation (with nonce, pool index, gossip mode, compression) not implemented | | `(s *Server) addRoute` | route.go:2113 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:496` (`Register`) | .NET registers route in dictionary; missing: pool index management, duplicate detection with `handleDuplicateRoute`, per-account route registration in `accRoutes`, `sendSubsToRoute` call, gateway/leafnode URL propagation, `forwardNewRouteInfoToKnownServers` | | `hasSolicitedRoute` | route.go:2438 | MISSING | — | Helper to find a solicited route in a pool slice; not implemented | | `upgradeRouteToSolicited` | route.go:2458 | MISSING | — | Upgrade an inbound route to solicited status; not implemented | | `handleDuplicateRoute` | route.go:2473 | MISSING | — | Duplicate route resolution (close extra connection, preserve retry flag) not implemented | | `(c *client) importFilter` | route.go:2510 | MISSING | — | Permission-based subscription filter for sending to routes not implemented | | `(s *Server) updateRouteSubscriptionMap` | route.go:2519 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:381,392` (`PropagateLocalSubscription`/`PropagateLocalUnsubscription`) | .NET broadcasts RS+/RS- to all routes; missing: account routePoolIdx-based routing, queue-weight dedup (`sqmu`/`lqws`), no-pool route handling, gateway/leafnode interest updates | | `(s *Server) startRouteAcceptLoop` | route.go:2696 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:333` (`StartAsync`) | .NET binds and starts accept loop, solicits configured routes; missing: cluster name logging, TLS config on accept, routeInfo construction, advertise/NoAdvertise, LeafNode/Gateway URL propagation | | `(s *Server) setRouteInfoHostPortAndIP` | route.go:2829 | MISSING | — | Route INFO host/port/IP with Cluster.Advertise support not implemented | | `(s *Server) StartRouting` | route.go:2849 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:333` (`StartAsync`) | Functionally equivalent: starts accept loop and solicits routes | | `(s *Server) reConnectToRoute` | route.go:2861 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:462` (`ConnectToRouteWithRetryAsync`) | .NET retries indefinitely with 250ms delay; missing: random jitter delay, explicit vs implicit distinction affecting delay, quit-channel integration | | `(s *Server) routeStillValid` | route.go:2881 | MISSING | — | Check that a route URL is still in configured routes list (for reconnect guard) not implemented | | `(s *Server) connectToRoute` | route.go:2890 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:462` (`ConnectToRouteWithRetryAsync`) | .NET connects and retries; missing: explicit/implicit distinction, ConnectRetries limit, exponential backoff (`ConnectBackoff`), `routesToSelf` exclusion, address randomization from DNS | | `(c *client) isSolicitedRoute` | route.go:2976 | MISSING | — | Helper predicate; not implemented | | `(s *Server) saveRouteTLSName` | route.go:2985 | NOT_APPLICABLE | — | TLS not yet implemented in .NET port | | `(s *Server) solicitRoutes` | route.go:2996 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:347–354` | .NET solicits configured routes with pool connections; missing: per-account (pinned) route solicitation, `saveRouteTLSName` | | `(c *client) processRouteConnect` | route.go:3011 | MISSING | — | Parsing and validation of inbound CONNECT from route (cluster name check, wrong-port detection, LNOC/LNOCU flags) not implemented; .NET uses a simpler handshake | | `(s *Server) removeAllRoutesExcept` | route.go:3085 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:602` (`RemoveAllRoutesExcept`) | Equivalent behavior: remove all routes not in the keep-set | | `(s *Server) removeRoute` | route.go:3113 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:577` (`RemoveRoute`) | .NET removes from `_routes` dict; missing: per-account route cleanup (`accRoutes`), hash removal, gateway/leafnode URL withdrawal, noPool counter, reconnect-after-noPool logic | | `(s *Server) isDuplicateServerName` | route.go:3233 | MISSING | — | Duplicate server name detection across routes not implemented | | `(s *Server) forEachNonPerAccountRoute` | route.go:3263 | NOT_APPLICABLE | — | Internal Go iterator over route slice; .NET uses `_routes.Values` LINQ directly | | `(s *Server) forEachRoute` | route.go:3277 | NOT_APPLICABLE | — | Internal Go iterator; .NET enumerates `_routes` and `_accountRoutes` directly | | `(s *Server) forEachRouteIdx` | route.go:3292 | NOT_APPLICABLE | — | Internal Go pool-index iterator; .NET `ComputeRoutePoolIdx` achieves equivalent selection | | `(s *Server) forEachRemote` | route.go:3305 | NOT_APPLICABLE | — | Internal Go iterator (first non-nil per remote); .NET has no equivalent but uses LINQ | --- ## Keeping This File Updated After porting work is completed: 1. **Update status**: Change `MISSING → PORTED` or `PARTIAL → PORTED` for each item completed 2. **Add .NET path**: Fill in the ".NET Equivalent" column with the actual file:line 3. **Re-count LOC**: Update the LOC numbers in `stillmissing.md`: ```bash # Re-count .NET source LOC for this module find src/NATS.Server/Routes/ -name '*.cs' -type f -exec cat {} + | wc -l # Re-count .NET test LOC for this module find tests/NATS.Server.Tests/Routes/ -name '*.cs' -type f -exec cat {} + | wc -l ``` 4. **Add a changelog entry** below with date and summary of what was ported 5. **Update the parity DB** if new test mappings were created: ```bash sqlite3 docs/test_parity.db "INSERT INTO test_mappings (go_test_id, dotnet_test_id, confidence, notes) VALUES (?, ?, 'manual', 'ported in YYYY-MM-DD session')" ``` ## Change Log | Date | Change | By | |------|--------|----| | 2026-02-25 | File created with LLM analysis instructions | auto | | 2026-02-25 | Full gap inventory populated: 57 Go symbols classified across route.go (3,314 lines). Counts: PORTED 4, PARTIAL 21, MISSING 23, NOT_APPLICABLE 9, DEFERRED 0 | auto |