Message Routing
When you publish a message using IMessageBus
or IMessageContext
, Wolverine uses its concept of subscriptions to know how and where to send the message. Consider this code that publishes a PingMessage
:
public class SendingExample
{
public async Task SendPingsAndPongs(IMessageContext bus)
{
// Publish a message
await bus.SendAsync(new PingMessage());
}
}
Routing Rules
INFO
There are some special message type routing for some Wolverine internal messages and the Marten event forwarding through its IEvent<T>
wrappers. Because of course there are some oddball exception cases.
When sending, publishing, scheduling, or invoking a message type for the first time, Wolverine runs through a series of rules to determine what endpoint(s) subscribe to the message type. Those rules in order of precedence are:
- Is the message type "forwarded" to another message type? If so, the routing uses the destination type. See message forwarding for more information.
- Are there any explicit routing rules that apply to this message type? If so, use only the subscriptions discovered from explicit rules (as explained in a following section).
- Use a local subscription using the conventional local queue routing if the message type has a known message handler within the application. This conventional routing to local queues can be disabled or made "additive" so that Wolverine also applies other conventional routing.
- Any registered message routing conventions like the Rabbit MQ or Amazon SQS routing conventions or a user defined routing convention.
Diagnostics
There's admittedly a lot of switches and options for message routing, and it's quite possible that the actual behavior could be confusing, especially with unusual configuration usages. Not to worry (too much), because Wolverine gives you a couple options to preview exactly what the subscriptions are for a given message type you can use to check your understanding of the Wolverine configuration.
Programmatically, this code shows how to "look" into the configured Wolverine subscriptions for a message type:
public static void PreviewRouting(IHost host)
{
// In test projects, you would probably have access to the IHost for
// the running application
// First, get access to the Wolverine runtime for the application
// It's registered by Wolverine as a singleton in your IoC container
var runtime = host.Services.GetRequiredService<IWolverineRuntime>();
var router = runtime.RoutingFor(typeof(MyMessage));
// If using Wolverine 3.6 or later when we added more
// ToString() behavior for exactly this reason
foreach (var messageRoute in router.Routes)
{
Debug.WriteLine(messageRoute);
}
// Otherwise, you might have to do this to "see" where
// the routing is going
foreach (var route in router.Routes.OfType<MessageRoute>())
{
Debug.WriteLine(route.Sender.Destination);
}
}
First, you can always use the command line support to preview Wolverine's known message types by using:
dotnet run -- describe
You might have to scroll a little bit, but there is a section that previews message subscriptions by type as a tabular output from that command.
TIP
The command line preview can only show subscriptions for the message types that Wolverine "knows" it will try to send at bootstrapping time. See Message Discovery for how to better utilize this preview functionality by "telling" Wolverine what your outgoing message types are.
Explicit Subscriptions
To route messages to specific endpoints, we can apply static message routing rules by using a routing rule as shown below:
using var host = Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
// Route a single message type
opts.PublishMessage<PingMessage>()
.ToServerAndPort("server", 1111);
// Send every possible message to a TCP listener
// on this box at port 2222
opts.PublishAllMessages().ToPort(2222);
// Or use a more fluent interface style
opts.Publish().MessagesFromAssembly(typeof(PingMessage).Assembly)
.ToPort(3333);
// Complicated rules, I don't think folks will use this much
opts.Publish(rule =>
{
// Apply as many message matching
// rules as you need
// Specific message types
rule.Message<PingMessage>();
rule.Message<Message1>();
// Implementing a specific marker interface or common base class
rule.MessagesImplementing<IEventMarker>();
// All types in a certain assembly
rule.MessagesFromAssemblyContaining<PingMessage>();
// or this
rule.MessagesFromAssembly(typeof(PingMessage).Assembly);
// or by namespace
rule.MessagesFromNamespace("MyMessageLibrary");
rule.MessagesFromNamespaceContaining<PingMessage>();
// Express the subscribers
rule.ToPort(1111);
rule.ToPort(2222);
});
// Or you just send all messages to a certain endpoint
opts.PublishAllMessages().ToPort(3333);
}).StartAsync();
Do note that doing the message type filtering by namespace will also include child namespaces. In our own usage we try to rely on either namespace rules or by using shared message assemblies.
Disabling Local Routing
Hey, it's perfectly possible that you want all messages going through external message brokers even when the message types all have known message handlers in the application. To do that, simply disable the automatic local message routing like this:
public static async Task disable_queue_routing()
{
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
// This will disable the conventional local queue
// routing that would take precedence over other conventional
// routing
opts.Policies.DisableConventionalLocalRouting();
// Other routing conventions. Rabbit MQ? SQS?
}).StartAsync();
This does allow you to possibly do better load balancing between application nodes.
Using Both Local Routing and External Broker Conventional Routing 3.6
You may want both the local routing conventions and external routing conventions to apply to the same message type. An early Wolverine user needed to both handle an event message created by their application locally inside that application (through a local queue), and to publish the same event message through external brokers to a different system. You can now make the local routing conventions be "additive" such that the message routing will also use external routing conventions even with local handlers like this:
var builder = Host.CreateApplicationBuilder();
builder.UseWolverine(opts =>
{
var rabbitConnectionString = builder
.Configuration.GetConnectionString("rabbitmq");
opts.UseRabbitMq(rabbitConnectionString)
.AutoProvision()
// Using the built in, default Rabbit MQ message routing conventions
.UseConventionalRouting();
// Allow Wolverine to *also* apply the Rabbit MQ conventional
// routing to message types that this system can handle locally
opts.Policies.ConventionalLocalRoutingIsAdditive();
});
Routing Internals
Wolverine has an internal model called IMessageRoute
that models a subscription for a message type that "knows" how to create the Wolverine Envelope
for a single outgoing message to a single subscribing endpoint:
/// <summary>
/// Contains all the rules for where and how an outgoing message
/// should be sent to a single subscriber
/// </summary>
public interface IMessageRoute
{
Envelope CreateForSending(object message, DeliveryOptions? options, ISendingAgent localDurableQueue,
WolverineRuntime runtime, string? topicName);
}
This type "knows" about any endpoint or model sending customizations like delivery expiration rules or in some cases, some user defined logic to determine the topic name for the message at runtime for message broker endpoints that support topic based publishing.
At runtime, when you decide to publish a message (and this applies to cascading messages in handlers), the workflow in the Wolverine internals is below:
As to how Wolverine determines the message routing, the internals are shown below:
Wolverine has a handful of built in IMessageRouteSource
implementations in precedence order:
TransformedMessageRouteSource
- only really used by the Marten event sourcing support to "know" to forward messages of typeIEvent<T>
to the event typeT
AgentMessages
- just for Wolverine's own internalIAgentCommand
commandsExplicitRouting
- explicitly defined subscriptions rules from the Wolverine fluent interface (PublishMessage<Foo>().ToRabbitQueue("foo")
)LocalRouting
- if a message type has a known handler type in the system, Wolverine will route the message to any local queues for that message typeMessageRoutingConventions
- this would be any message routing conventions enabled for external transport brokers like Rabbit MQ or Azure Service Bus. This could also be a custom message routing convention.
Rolling Your own Messaging Convention
Let's say you want to use a completely different conventional routing topology than anything Wolverine provides out of the box. You can do that by creating your own implementation of this interface:
/// <summary>
/// Plugin for doing any kind of conventional message routing
/// </summary>
public interface IMessageRoutingConvention
{
/// <summary>
/// Use this to define listening endpoints based on the known message handlers for the application
/// </summary>
/// <param name="runtime"></param>
/// <param name="handledMessageTypes"></param>
void DiscoverListeners(IWolverineRuntime runtime, IReadOnlyList<Type> handledMessageTypes);
/// <summary>
/// Create outgoing subscriptions for the application for the given message type
/// </summary>
/// <param name="messageType"></param>
/// <param name="runtime"></param>
/// <returns></returns>
IEnumerable<Endpoint> DiscoverSenders(Type messageType, IWolverineRuntime runtime);
}
As a concrete example, the Wolverine team received this request to conventionally route messages based on the message type name to a Rabbit MQ exchange and routing key. That's not something that Wolverine supports out of the box, but you could build your own simplistic routing convention like this:
public class RouteKeyConvention : IMessageRoutingConvention
{
private readonly string _exchangeName;
public RouteKeyConvention(string exchangeName)
{
_exchangeName = exchangeName;
}
public void DiscoverListeners(IWolverineRuntime runtime, IReadOnlyList<Type> handledMessageTypes)
{
// Not worrying about this at all for this case
}
public IEnumerable<Endpoint> DiscoverSenders(Type messageType, IWolverineRuntime runtime)
{
var routingKey = messageType.FullNameInCode().ToLowerInvariant();
var rabbitTransport = runtime.Options.Transports.GetOrCreate<RabbitMqTransport>();
// Find or create the named Rabbit MQ exchange in Wolverine's model
var exchange = rabbitTransport.Exchanges[_exchangeName];
// Find or create the named routing key / binding key
// in Wolverine's model
var routing = exchange.Routings[routingKey];
// Tell Wolverine you want the message type routed to this
// endpoint
yield return routing;
}
}
And register it to your Wolverine application like so:
var builder = Host.CreateApplicationBuilder();
var rabbitConnectionString = builder
.Configuration
.GetConnectionString("rabbitmq");
builder.UseWolverine(opts =>
{
opts.UseRabbitMq(rabbitConnectionString)
.AutoProvision();
var exchangeName = builder
.Configuration
.GetValue<string>("exchange-name");
opts.RouteWith(new RouteKeyConvention(exchangeName));
});
// actually start the app...