Summary
In Wolverine 6.x, WolverineRuntime.PrepopulateRoutingCache (new in 6.0, runs during StartAsync) crashes host startup with a NotSupportedException from AzureServiceBusSubscription.CreateSender for any message type whose only handler is bound to an Azure Service Bus subscription via AddStickyHandler. This is a regression from 5.x, where routes were built lazily on first send rather than eagerly at startup, so the same configuration worked fine there.
Root cause (traced)
PrepopulateRoutingCache iterates every discovered message type and calls RoutingFor(messageType), which eventually reaches LocalRouting.FindRoutes:
// WolverineRuntime.Routing.cs
if (options.HandlerGraph.CanHandle(messageType))
{
var endpoints = options.LocalRouting.DiscoverSenders(messageType, runtime).ToList();
...
return endpoints.Select(e => MessageRoute.For(messageType, e, runtime));
}
LocalTransport.DiscoverSenders includes every endpoint the message type's handler chain is sticky-bound to:
// LocalTransport.cs
foreach (var endpoint in chain.ByEndpoint.SelectMany(x => x.Endpoints))
yield return endpoint;
chain.ByEndpoint is populated by AddStickyHandler. So any endpoint sticky-bound to a handler — including an Azure Service Bus subscription, which is receive-only and has never supported sending — gets treated as a "local sending candidate." MessageRoute.For then calls Endpoint.StartSending → CreateSender, and AzureServiceBusSubscription.CreateSender unconditionally throws:
protected override ISender CreateSender(IWolverineRuntime runtime)
{
throw new NotSupportedException();
}
This conflates "sticky-bound for inbound delivery" with "valid outbound local-routing target." A subscription being the exclusive delivery channel for a handler says nothing about whether anything should ever be able to send to it locally.
Minimal repro
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.UseAzureServiceBus(connectionString);
opts.ListenToAzureServiceBusSubscription("my-subscription")
.FromTopic("my-topic")
.RequireSessions()
.ProcessInline()
.AddStickyHandler(typeof(MyHandler));
})
.StartAsync();
MyMessage (the type MyHandler handles) is never explicitly published or sent anywhere else in the application. StartAsync() still throws:
System.NotSupportedException: Specified method is not supported.
at Wolverine.AzureServiceBus.Internal.AzureServiceBusSubscription.CreateSender(IWolverineRuntime runtime)
at Wolverine.Configuration.Endpoint.StartSending(IWolverineRuntime runtime, Uri replyUri)
at Wolverine.Configuration.EndpointCollection.buildSendingAgent(Uri uri, Action`1 configureNewEndpoint)
at Wolverine.Configuration.EndpointCollection.GetOrBuildSendingAgent(Uri address, Action`1 configureNewEndpoint)
at Wolverine.Runtime.Routing.MessageRoute..ctor(Type messageType, Endpoint endpoint, IWolverineRuntime runtime)
at Wolverine.Runtime.Routing.MessageRoute.For(Type messageType, Endpoint endpoint, IWolverineRuntime runtime)
at Wolverine.Runtime.LocalRouting.<>c__DisplayClass0_0.<FindRoutes>b__2(Endpoint e)
at Wolverine.Runtime.WolverineRuntime.findRoutes(Type messageType)
at Wolverine.Runtime.WolverineRuntime.RoutingFor(Type messageType)
at Wolverine.Runtime.WolverineRuntime.PrepopulateRoutingCache(IEnumerable`1 messageTypes)
at Wolverine.Runtime.WolverineRuntime.StartAsync(CancellationToken cancellationToken)
Impact
RequireSessions()+ProcessInline()+AddStickyHandler() on an ASB subscription is a documented, common pattern for fanning one topic out to several session-scoped, exclusively-bound handlers (avoiding Wolverine's default global-by-type dispatch running every sticky handler for every message). Any application using this pattern on 6.x will crash at startup, in every environment, the moment PrepopulateRoutingCache runs — this isn't confined to a narrow edge case.
Suggested fix direction
LocalTransport.DiscoverSenders's "sticky assignments" branch should not yield an endpoint that structurally cannot act as a sender (e.g. filter on whatever capability/interface distinguishes a listen-only endpoint), or PrepopulateRoutingCache/MessageRoute.For should tolerate a NotSupportedException/similar signal from CreateSender for a sticky-bound endpoint rather than letting it fault host startup — a sticky binding's purpose is inbound delivery routing, not proof that a local send target exists.
Environment
- WolverineFx 6.39.1
- Wolverine.AzureServiceBus 6.39.1
- Confirmed absent in 5.39.0 —
PrepopulateRoutingCache does not exist in that version's WolverineRuntime.HostService.cs at all; routes were built lazily there, so the same configuration never hit this path.
Summary
In Wolverine 6.x,
WolverineRuntime.PrepopulateRoutingCache(new in 6.0, runs duringStartAsync) crashes host startup with aNotSupportedExceptionfromAzureServiceBusSubscription.CreateSenderfor any message type whose only handler is bound to an Azure Service Bus subscription viaAddStickyHandler. This is a regression from 5.x, where routes were built lazily on first send rather than eagerly at startup, so the same configuration worked fine there.Root cause (traced)
PrepopulateRoutingCacheiterates every discovered message type and callsRoutingFor(messageType), which eventually reachesLocalRouting.FindRoutes:LocalTransport.DiscoverSendersincludes every endpoint the message type's handler chain is sticky-bound to:chain.ByEndpointis populated byAddStickyHandler. So any endpoint sticky-bound to a handler — including an Azure Service Bus subscription, which is receive-only and has never supported sending — gets treated as a "local sending candidate."MessageRoute.Forthen callsEndpoint.StartSending→CreateSender, andAzureServiceBusSubscription.CreateSenderunconditionally throws:This conflates "sticky-bound for inbound delivery" with "valid outbound local-routing target." A subscription being the exclusive delivery channel for a handler says nothing about whether anything should ever be able to send to it locally.
Minimal repro
MyMessage(the typeMyHandlerhandles) is never explicitly published or sent anywhere else in the application.StartAsync()still throws:Impact
RequireSessions()+ProcessInline()+AddStickyHandler()on an ASB subscription is a documented, common pattern for fanning one topic out to several session-scoped, exclusively-bound handlers (avoiding Wolverine's default global-by-type dispatch running every sticky handler for every message). Any application using this pattern on 6.x will crash at startup, in every environment, the momentPrepopulateRoutingCacheruns — this isn't confined to a narrow edge case.Suggested fix direction
LocalTransport.DiscoverSenders's "sticky assignments" branch should not yield an endpoint that structurally cannot act as a sender (e.g. filter on whatever capability/interface distinguishes a listen-only endpoint), orPrepopulateRoutingCache/MessageRoute.Forshould tolerate aNotSupportedException/similar signal fromCreateSenderfor a sticky-bound endpoint rather than letting it fault host startup — a sticky binding's purpose is inbound delivery routing, not proof that a local send target exists.Environment
PrepopulateRoutingCachedoes not exist in that version'sWolverineRuntime.HostService.csat all; routes were built lazily there, so the same configuration never hit this path.