Skip to content

feat(nats): add named-broker support (GH-3310)#3314

Merged
jeremydmiller merged 2 commits into
mainfrom
feat/nats-named-broker-3310
Jul 6, 2026
Merged

feat(nats): add named-broker support (GH-3310)#3314
jeremydmiller merged 2 commits into
mainfrom
feat/nats-named-broker-3310

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #3310.

First of the transport multi-broker parity sweep (issues #3303#3310, seeded by NATS #3283). Brings the NATS transport to parity with RabbitMQ/Kafka/SQS/Azure on the named broker feature: a single application can address multiple independent NATS brokers, each pinnable via *OnNamedBroker overloads.

Changes

  • NatsTransport(string protocol) constructor so the transport can be created through TransportCollection.GetOrCreate<T>(BrokerName). A named broker's Protocol doubles as its endpoints' URI scheme. The parameterless ctor delegates to it with ProtocolName ("nats"), unchanged behavior for the default broker.
  • Scheme-aware URIs: endpoint URIs (NatsEndpoint) and inbound envelope Destination/ReplyUri (NatsEnvelopeMapper + JetStreamEnvelopeMapper) are now built from the transport's Protocol instead of a hard-coded "nats" literal, so a named broker's endpoints never collide with the default nats:// broker and reply/tracking routing resolves back to the right transport instance.
  • Relaxed ExtractSubjectFromUri: the scheme is no longer validated against a fixed literal (named brokers carry the broker name as the scheme; routing to the correct transport already happens by scheme upstream).
  • NatsHealthCheck.Protocol now reflects the transport's Protocol.
  • New extension methods: AddNamedNatsBroker (connection-string and Action<NatsTransportConfiguration> overloads), ToNatsSubjectOnNamedBroker, ListenToNatsSubjectOnNamedBroker.

Tests

Wolverine.Nats.Tests — full suite green (144 on net9.0):

  • CI-safe (NatsNamedBrokerRegistrationTests, no broker): distinct transport instance per broker, named-broker endpoints use the broker name as their URI scheme, config-action overload applies to the named broker only.
  • Integration (NatsNamedBrokerTests, spins a second broker via Testcontainers, mirrors NatsPerTenantConnectionTests): a named send lands on the named broker and not the default; a default send lands on the default and not the named; a round-trip over the named broker arrives stamped with the broker's scheme (exercises the inbound mapper change).

Docs

New "Connecting to Multiple NATS Brokers" section in docs/guide/messaging/transports/nats.md, contrasting named brokers (static, explicitly targeted) with per-tenant connections (runtime tenant-id selection).

Build

dotnet build wolverine.slnx -c Release0 errors.

🤖 Generated with Claude Code

jeremydmiller and others added 2 commits July 5, 2026 17:19
Bring the NATS transport to parity with RabbitMQ/Kafka/SQS/Azure on the
"named broker" feature so a single application can address multiple
independent NATS brokers, each pinnable via *OnNamedBroker overloads.

- Add a NatsTransport(string protocol) constructor so the transport can be
  created through TransportCollection.GetOrCreate<T>(BrokerName). A named
  broker's Protocol doubles as its endpoints' URI scheme.
- Build endpoint URIs and inbound envelope Destination/ReplyUri from the
  transport's Protocol instead of a hard-coded "nats" literal, so a named
  broker's endpoints never collide with the default nats:// broker and
  reply/tracking routing resolves back to the right transport instance.
- Relax NatsTransport.ExtractSubjectFromUri: the scheme is no longer
  validated against a fixed literal (named brokers carry the broker name as
  the scheme; routing by scheme already happened upstream).
- NatsHealthCheck.Protocol now reflects the transport's Protocol.
- New extension methods: AddNamedNatsBroker (connection-string and
  Action<NatsTransportConfiguration> overloads), ToNatsSubjectOnNamedBroker,
  ListenToNatsSubjectOnNamedBroker.

Tests: 3 CI-safe registration/URI tests + 3 integration tests that spin up a
second broker via Testcontainers proving a named send lands on the named
broker (not the default), a default send lands on the default (not the
named), and a round-trip over the named broker stamps the broker's scheme.
Full Wolverine.Nats suite green (144). Docs: new "Connecting to Multiple
NATS Brokers" section in nats.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…est/reply test

Addresses review feedback questioning whether the NatsEnvelopeMapper scheme
change was necessary. It was not: on a received message the pipeline already
sets the correct scheme without touching the mapper.

- Envelope.MarkReceived sets Destination from the listener endpoint's URI
  (NatsListener.Address = NatsEndpoint.Uri), which already carries the named
  broker's scheme via the NatsEndpoint(protocol) constructor change.
- MessageContext.ReadEnvelope calls MaybeCorrectReplyUri, which rewrites the
  reply-uri's scheme to match Destination.Scheme (JasperFx MaybeCorrectScheme
  unconditionally swaps the scheme). MarkReceived runs before ReadEnvelope, so
  by then Destination already has the named broker's scheme.

So reverting NatsEnvelopeMapper/JetStreamEnvelopeMapper back to the canonical
nats:// form is correct — the endpoint-URI scheme is the single source of
truth and the pipeline propagates it to Destination and the reply-uri.

Adds request_reply_round_trips_over_the_named_broker: a full InvokeAndWaitAsync
over the named broker. If reply routing resolved to the default broker instead
of the named one, this would time out — so it proves the reply travels back
over the named broker with the mapper untouched.
@jeremydmiller

Copy link
Copy Markdown
Member Author

Re: whether the inbound envelope-mapper scheme change was necessary — it wasn't, so I dropped it (NatsEnvelopeMapper / JetStreamEnvelopeMapper are now byte-identical to main) in follow-up commit 82a967d.

Trace of why the endpoint-URI change alone is sufficient:

  • Envelope.MarkReceived (run by the receiver on every inbound message) sets Destination = listener.Address, and NatsListener.Address = NatsEndpoint.Uri — which already carries the named broker's scheme from the NatsEndpoint(protocol) constructor change. So it overwrites whatever the mapper stamped for Destination.
  • MessageContext.ReadEnvelope then calls MaybeCorrectReplyUri(), which rewrites the reply-uri's scheme to match Destination.Scheme. JasperFx's MaybeCorrectScheme swaps the scheme unconditionally (scheme + "://" + uri.Authority + uri.PathAndQuery). MarkReceived runs before ReadEnvelope, so by then Destination is already the named scheme.

So the endpoint URI is the single source of truth and the pipeline propagates it to both Destination and the reply-uri.

Added request_reply_round_trips_over_the_named_broker to lock this in: a full InvokeAndWaitAsync over the named broker. If reply routing resolved to the default broker it would time out — it passes with the mapper untouched, proving the reply travels back over the named broker. Full Wolverine.Nats suite green (145).

@jeremydmiller
jeremydmiller marked this pull request as ready for review July 6, 2026 11:01
@jeremydmiller
jeremydmiller merged commit 2549174 into main Jul 6, 2026
24 of 25 checks passed
This was referenced Jul 9, 2026
This was referenced Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NATS: add named-broker support

1 participant