fix(rabbitmq): bind handler queue to every handled-message exchange under FromHandlerType#2697
Merged
jeremydmiller merged 1 commit intomainfrom May 7, 2026
Conversation
…nder FromHandlerType When `UseConventionalRouting(NamingSource.FromHandlerType)` was used and a single handler type accepted two or more message types (typical for sagas or any aggregate-style handler), the handler-named queue ended up bound to only the FIRST message type's exchange. The remaining message types were silently dropped on the floor — messages published to those exchanges never reached the handler queue. Root cause: `RabbitMqMessageRoutingConvention.ApplyListenerRoutingDefaults` short-circuited on `queue.HasBindings`. The base `MessageRoutingConvention.DiscoverListeners` calls this method once per (handlerType, messageType) pair under FromHandlerType naming, so the second pass saw the binding the first pass had just added and skipped its own binding entirely. The original guard's intent was protecting user-configured custom bindings (set up via `RabbitMqConventionalListenerConfiguration` in the `ConfigureListeners` lambda, which runs *before* this method) — the convention shouldn't double-bind on top of an explicit user binding. The fix narrows the guard to per-exchange dedup so the multi-message-type case binds correctly while the protected case still suppresses cleanly. Closes #2681. Tests: - `Bug_2681_handler_type_naming_binds_all_exchanges.handler_queue_is_bound_to_every_handled_message_exchange` — primary reproducer. A single `MultiMessageHandler` handles `MultiMessageFoo` + `MultiMessageBar`; pre-fix the handler queue was bound only to whichever message-type exchange got processed second. Verified red against main, green with the fix. - `Bug_2681_handler_type_naming_binds_all_exchanges.both_message_exchanges_were_created` — sanity check that both exchanges actually get registered (the bug was purely on the queue-binding side). - `Bug_2681_custom_bindings_are_not_double_added.user_custom_binding_is_not_double_added_by_the_convention` — guard that the new per-exchange dedup still suppresses the default convention binding when a user has pre-configured a custom binding to the same exchange. - Full `ConventionalRouting` suite: 31/31 passed on net9.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 #2681.
Summary
UseConventionalRouting(NamingSource.FromHandlerType), when a single handler type accepts two or more message types (typical for sagas or any aggregate-style handler), the handler-named queue ended up bound to only the FIRST message type's exchange. Messages published to the remaining exchanges silently never reached the handler queue.RabbitMqMessageRoutingConvention.ApplyListenerRoutingDefaultsshort-circuited onqueue.HasBindings. The baseMessageRoutingConvention.DiscoverListenerscalls this method once per (handlerType, messageType) pair underFromHandlerTypenaming — so the second pass saw the binding the first pass had just added and silently skipped its own.ConfigureListenerslambda, which runs beforeApplyListenerRoutingDefaults) from being double-bound on top of by the convention. Narrowing the guard to per-exchange dedup preserves that protected case while letting the multi-message-type case bind correctly.What changed
src/Transports/RabbitMQ/Wolverine.RabbitMQ/RabbitMqMessageRoutingConvention.cs:23-50:The new guard is strictly weaker than the old one — every case the old guard silenced still gets silenced. The bug case is the only behavioural delta.
Tests
Bug_2681_handler_type_naming_binds_all_exchanges.handler_queue_is_bound_to_every_handled_message_exchange— primary reproducer. A singleMultiMessageHandlerhandlesMultiMessageFoo+MultiMessageBar; pre-fix the handler queue was bound only to whichever message-type exchange got processed second. Verified red on main (queue bound only toMultiMessageBar); green with the fix.Bug_2681_handler_type_naming_binds_all_exchanges.both_message_exchanges_were_created— sanity check that both exchanges actually get registered (the bug was purely on the queue-binding side).Bug_2681_custom_bindings_are_not_double_added.user_custom_binding_is_not_double_added_by_the_convention— guard that the new per-exchange dedup still suppresses the default convention binding when a user has pre-configured a custom binding to the same exchange viaConfigureListeners+BindToExchange<TMessage>(...).Test plan
ConventionalRoutingsuite — 31/31 passed on net9.0🤖 Generated with Claude Code