feat(mqtt): named-broker and broker-per-tenant support (GH-3307)#3319
Merged
Conversation
Brings the MQTT transport to parity with RabbitMQ/NATS/SQS on the two
established multi-broker features:
Named broker
- MqttTransport(string protocol) ctor + MqttTransport() : this("mqtt");
MqttTopic/MqttEndpointUri build the endpoint URI from parent.Protocol so
a named broker's endpoints get their own URI scheme and never collide
with the default mqtt:// broker.
- AddNamedMqttBroker + ListenToMqttTopicOnNamedBroker + ToMqttTopicOnNamedBroker;
internal MqttTransport(this WolverineOptions, BrokerName?) accessor.
- MqttHealthCheck.Protocol now follows the transport's Protocol.
Broker-per-tenant
- New MqttTenant (own ManagedMqttClientOptions / IManagedMqttClient) and
MqttTopicSender (fire-and-forget ISender bound to a specific client, kept
callback-free per GH-2361).
- MqttTransport gains a per-tenant client cache + TenantedIdBehavior; each
tenant client is created in InitializeAsync with a UNIQUE ClientId
(<id>-tenant-<tenantId>) so the broker never kicks it, and is stopped in
DisposeAsync. Listener bookkeeping is now per-managed-client so tenant
connections resolve their own (tenant-id-stamping) listeners.
- MqttTopic.CreateSender returns a TenantedSender when tenants exist;
BuildListenerAsync builds a CompoundListener with one ReceiverWithRules
listener per tenant client.
- MqttTransportExpression.TenantIdBehavior + AddTenant overloads.
Adds Wolverine.MQTT to InternalsVisibleTo for TenantIdRule/ReceiverWithRules.
Tests (two LocalMqttBroker instances on free ports, no Docker): named-broker
routing + round-trip + request/reply, and per-tenant routing + inbound
tenant-id stamping + unique client-id + TenantedSender resolution. Docs:
"Connecting to Multiple MQTT Brokers" and "Broker per Tenant" sections.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jeremydmiller
marked this pull request as ready for review
July 6, 2026 03:06
This was referenced Jul 9, 2026
Merged
This was referenced Jul 14, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3307
Brings the MQTT transport up to parity with RabbitMQ / NATS / SQS on the two established multi-broker features. MQTT had neither before; this adds both.
Named broker
MqttTransport(string protocol)ctor (+MqttTransport() : this("mqtt")), reached viaTransportCollection.GetOrCreate<T>(BrokerName).MqttTopic/MqttEndpointUribuild the endpoint URI fromparent.Protocolinstead of a hard-codedmqtt://literal — the key change so a named broker's endpoints get their own URI scheme and don't collide with the default broker.AddNamedMqttBroker(BrokerName, …)(builder +ManagedMqttClientOptionsoverloads),ListenToMqttTopicOnNamedBroker,ToMqttTopicOnNamedBroker; internalMqttTransport(this WolverineOptions, BrokerName?)accessor.MqttHealthCheck.Protocolnow follows the transport'sProtocol.Broker-per-tenant
MqttTenant(ownManagedMqttClientOptions/IManagedMqttClient) andMqttTopicSender(a fire-and-forgetISenderbound to a specific client; kept callback-free per Message is stuck in outbox with multiple tenants and durable messaging #2361 sinceTenantedSenderdoesn't forwardRegisterCallback).MqttTransportgains a per-tenant client cache +TenantedIdBehavior. Each tenant client is created inInitializeAsyncwith a uniqueClientId(<id>-tenant-<tenantId>) — enforced even if the user pre-set one, because MQTT brokers kick a duplicate ClientId — and stopped inDisposeAsync. Listener bookkeeping is now per-managed-client so messages arriving on a tenant connection resolve that tenant's own (tenant-id-stamping) listener.MqttTopic.CreateSenderreturns aTenantedSenderwhen tenants exist;BuildListenerAsyncbuilds aCompoundListenerwith oneReceiverWithRules([new TenantIdRule(tenant)])listener per tenant client.MqttTransportExpression.TenantIdBehavior(...)+AddTenant(...)overloads.Wolverine.MQTTtoInternalsVisibleToforTenantIdRule/ReceiverWithRules.Tests
No Docker needed — two in-process
LocalMqttBrokerinstances on free ports simulate two brokers.named_broker_tests.cs+ registration tests): distinct transport per broker, broker-name URI scheme, publish lands on the named broker (raw MQTTnet subscriber) not the default and vice-versa, a consumed message is stamped with the named broker's scheme, and a full request/reply round-trips over the named broker.mqtt_per_tenant_broker_tests.cs): aDeliveryOptions { TenantId }message lands on the tenant's broker not the shared one, default falls back to the shared broker, inbound tenant message consumed and stamped withTenantId, unique tenant ClientId, and the endpoint resolves aTenantedSender.All 12 new tests pass; both existing compliance suites (47 tests) pass in isolation.
dotnet build wolverine.slnx -c Releaseis clean (0 warnings, 0 errors).Caveats (documented)
Docs: adds "Connecting to Multiple MQTT Brokers" and "Broker per Tenant" sections to
docs/guide/messaging/transports/mqtt.md.🤖 Generated with Claude Code