Multi-Transport Routing
Dispatch supports routing messages to multiple transports based on type or predicate rules. All transport routing is configured through the unified UseRouting() API.
Before You Start
- .NET 10.0
- Install Excalibur.Dispatch plus two or more transport packages:
dotnet add package Excalibur.Dispatchdotnet add package Excalibur.Dispatch.Transport.Kafkadotnet add package Excalibur.Dispatch.Transport.RabbitMQ
- Familiarity with choosing a transport and routing patterns
Quick Start
builder.Services.AddPluggableSerialization(); // Transports don't seat a default serializer
builder.Services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);
dispatch.UseRouting(routing =>
{
routing.Transport
.Route<OrderCreatedEvent>().To("kafka")
.Route<PaymentProcessedEvent>().To("rabbitmq")
.Default("rabbitmq");
});
});
// Register each transport with fluent builder pattern
builder.Services.AddKafkaTransport(kafka =>
{
kafka.BootstrapServers("localhost:9092")
.MapTopic<OrderCreatedEvent>("dispatch-events");
});
builder.Services.AddRabbitMQTransport(rmq =>
{
rmq.HostName("localhost")
.Port(5672)
.Credentials("guest", "guest")
.ConfigureExchange(exchange => exchange.Name("dispatch-events"));
});
Bulk Routing with RouteAll
For scenarios with many message types routed to the same transport, use RouteAll:
dispatch.UseRouting(routing =>
{
// Route multiple types in a single call
routing.Transport.RouteAll("kafka",
typeof(OrderCreatedEvent),
typeof(PaymentProcessedEvent),
typeof(ShipmentDispatchedEvent),
typeof(InventoryUpdatedEvent));
// Mix with individual routing
routing.Transport
.Route<AuditLogEvent>().To("rabbitmq")
.Default("rabbitmq");
});
RouteAll validates each type at registration time -- types that don't implement IIntegrationEvent throw ArgumentException immediately. This method uses reflection and carries [RequiresUnreferencedCode].
Transport Names
Each transport registers with a default name. Use these names in routing rules:
| Transport | Default Name | Registration Method |
|---|---|---|
| Kafka | "kafka" | AddKafkaTransport() |
| RabbitMQ | "rabbitmq" | AddRabbitMQTransport() |
| Azure Service Bus | "azure-servicebus" | AddAzureServiceBusTransport() |
| AWS SQS | "aws-sqs" | AddAwsSqsTransport() |
| Google Pub/Sub | "google-pubsub" | AddGooglePubSubTransport() |
| In-Memory | "inmemory" | AddInMemoryTransport() |
You can also register named instances:
builder.Services.AddPluggableSerialization(); // Transports don't seat a default serializer
builder.Services.AddKafkaTransport("kafka-orders", kafka =>
{
kafka.BootstrapServers("localhost:9092")
.MapTopic<OrderCreatedEvent>("orders");
});
builder.Services.AddKafkaTransport("kafka-analytics", kafka =>
{
kafka.BootstrapServers("localhost:9092")
.MapTopic<AnalyticsEvent>("analytics");
});
What a name isolates, and what it does not
Each named registration configures its own options instance, and that is what the transport under that name reads. Two named registrations of the same transport can carry different queues, topics, regions, credentials, or encryption keys, and neither overwrites the other.
This did not always hold. A named registration used to write the same unnamed options instance as every other registration of that transport, so whichever registered last silently supplied the settings for both — two SQS transports named for different queues ran against one queue, and a per-transport KMS key could not reach its own queue. If you worked around this by splitting hosts, or by keeping the two configurations deliberately identical, you can stop.
What a name does not isolate: the underlying provider client and message bus. Those are registered by type, first-registration-wins, so two named registrations of one transport still share a single client and bus instance. In practice that means:
| Per name | Shared across every name of that transport |
|---|---|
| Options — queue/topic, region, endpoints, FIFO selectors, payload limits, encryption key | The provider SDK client (for example one IAmazonSQS) |
| The keyed sender and receiver routing resolves | The message bus singleton |
| Start-up services that apply configuration to the broker |
So two names against different queues on one account work as you expect. Two names that need different credentials, different accounts, or different broker endpoints do not — the shared client is built once, from the first registration. Register the client yourself before the transports if you need to control it, or keep genuinely separate connections in separate hosts.
Routing Strategies
Transport routing only accepts IIntegrationEvent types. Commands and domain events cannot be routed to transports — they are handled locally. See Routing for details.
Route by Message Type
dispatch.UseRouting(routing =>
{
routing.Transport
.Route<OrderCreatedEvent>().To("kafka")
.Route<PaymentReceivedEvent>().To("kafka")
.Route<NotificationSentEvent>().To("rabbitmq")
.Default("rabbitmq");
});
Conditional Routing
Route messages based on message content using .When() predicates:
dispatch.UseRouting(routing =>
{
routing.Transport
// Route high-value orders to priority queue
.Route<OrderCreatedEvent>()
.When(order => order.TotalAmount > 10000).To("rabbitmq-priority")
// Route standard orders to regular queue
.Route<OrderCreatedEvent>().To("rabbitmq")
.Default("rabbitmq");
});
Context-aware predicates have access to IMessageContext:
dispatch.UseRouting(routing =>
{
routing.Transport
.Route<OrderCreatedEvent>()
.When((msg, ctx) => ctx.Items.ContainsKey("express")).To("kafka")
.Route<OrderCreatedEvent>().To("rabbitmq")
.Default("rabbitmq");
});
Default Transport
dispatch.UseRouting(routing =>
{
routing.Transport
// Specific routes evaluated first
.Route<OrderCreatedEvent>().To("kafka")
// Default for all unmatched message types
.Default("rabbitmq");
});
Route Evaluation Order
Routes are evaluated in this order:
- Type-specific routes —
Route<T>()matching the exact message type - Conditional routes —
.When()predicates evaluated in registration order - Default transport —
.Default()
The first matching route determines the target transport.
Real-World Scenarios
High-Volume Events to Kafka, Default to RabbitMQ
dispatch.UseRouting(routing =>
{
routing.Transport
// High-volume integration events -> Kafka for streaming/analytics
.Route<OrderCreatedEvent>().To("kafka")
.Route<PaymentProcessedEvent>().To("kafka")
.Route<InventoryUpdatedEvent>().To("kafka")
// Default -> RabbitMQ for all other integration events
.Default("rabbitmq");
});
Environment-Specific Routing
dispatch.UseRouting(routing =>
{
if (builder.Environment.IsDevelopment())
{
// Local: everything in-memory
routing.Transport.Default("inmemory");
}
else
{
routing.Transport
.Route<OrderCreatedEvent>().To("kafka")
.Default("rabbitmq");
}
});
Combined Transport and Endpoint Routing
UseRouting() provides both transport selection and endpoint routing in a single configuration:
dispatch.UseRouting(routing =>
{
// Tier 1: Which message bus?
routing.Transport
.Route<OrderCreatedEvent>().To("rabbitmq")
.Route<AnalyticsEvent>().To("kafka")
.Default("rabbitmq");
// Tier 2: Which services receive this?
routing.Endpoints
.Route<OrderCreatedEvent>()
.To("billing-service", "inventory-service");
routing.Fallback.To("dead-letter-queue");
});
See Message Routing for the full UseRouting() API including endpoint routing and fallback configuration.
Google Pub/Sub Routing
Google Pub/Sub uses the default name "google-pubsub":
builder.Services.AddGooglePubSubTransport("dispatch-events", pubsub =>
{
pubsub.ProjectId("my-gcp-project")
.TopicId("dispatch-events")
.SubscriptionId("dispatch-events-sub");
});
dispatch.UseRouting(routing =>
{
routing.Transport
.Route<OrderCreatedEvent>().To("google-pubsub")
.Default("rabbitmq");
});
Diagnostics
var router = serviceProvider.GetRequiredService<IDispatchRouter>();
// Check available routes for a message
var routes = router.GetAvailableRoutes(message, context);
// Check if a destination is configured for a message type
var canRoute = router.CanRouteTo(message, "billing-service");
Next Steps
- Kafka Transport — High-throughput streaming
- RabbitMQ Transport — Flexible routing patterns
- Azure Service Bus — Azure-native messaging
- Message Routing — Full UseRouting() API reference
See Also
- Transports Overview - All available transport providers
- Choosing a Transport - Comparison guide for transport selection
- Message Mapping - Transform messages per transport